This commit is contained in:
Ovidiu U
2026-05-12 09:47:26 +01:00
parent 3d103f19e1
commit 759e4f2784
183 changed files with 20094 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
<?php
test('the application returns a successful response', function () {
$response = $this->get('/');
$response->assertStatus(200);
});

View File

@@ -0,0 +1,128 @@
<?php
use App\Enums\ResponseStatus;
use App\Models\Website;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\postJson;
beforeEach(function (): void {
$this->website = Website::factory()->create([
'domain' => 'example.com',
'is_active' => true,
]);
$this->token = $this->website->createToken('test')->plainTextToken;
$this->headers = [
'Authorization' => "Bearer {$this->token}",
'Origin' => 'https://example.com',
];
});
test('requires authentication', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => ['name' => 'John Doe'],
])->assertUnauthorized();
});
test('requires registration number', function (): void {
postJson('/api/contact', [
'contact_data' => ['name' => 'John Doe'],
], $this->headers)
->assertUnprocessable()
->assertJsonValidationErrors(['registration_number']);
});
test('rejects invalid registration number format', function (): void {
postJson('/api/contact', [
'registration_number' => 'ovidiu',
'contact_data' => ['name' => 'John Doe'],
], $this->headers)
->assertUnprocessable()
->assertJsonValidationErrors(['registration_number']);
});
test('requires contact data', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
], $this->headers)
->assertUnprocessable()
->assertJsonValidationErrors(['contact_data']);
});
test('stores contact submission and returns success', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => [
'name' => 'John Doe',
'email' => 'john@example.com',
'phone' => '07700900000',
'address' => '1 High Street',
'message' => 'Please call me back.',
],
], $this->headers)
->assertSuccessful()
->assertJson(['success' => true]);
assertDatabaseHas('api_requests', [
'website_id' => $this->website->id,
'registration_number' => 'AB12CDE',
'response_status' => ResponseStatus::ContactSubmitted->value,
]);
});
test('requires at least email or phone in contact data', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => ['name' => 'John Doe'],
], $this->headers)
->assertUnprocessable()
->assertJsonValidationErrors(['contact_data.email', 'contact_data.phone']);
});
test('accepts contact data with email only', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => ['email' => 'john@example.com'],
], $this->headers)->assertSuccessful();
});
test('accepts contact data with phone only', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => ['phone' => '07700900000'],
], $this->headers)->assertSuccessful();
});
test('normalises registration number before storing', function (): void {
postJson('/api/contact', [
'registration_number' => 'ab12 cde',
'contact_data' => ['email' => 'john@example.com', 'phone' => '07700900000'],
], $this->headers)->assertSuccessful();
assertDatabaseHas('api_requests', [
'registration_number' => 'AB12CDE',
'response_status' => ResponseStatus::ContactSubmitted->value,
]);
});
test('inactive website cannot submit contact', function (): void {
$this->website->update(['is_active' => false]);
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => ['name' => 'John Doe'],
], $this->headers)->assertForbidden();
});
test('rejects request from wrong origin', function (): void {
postJson('/api/contact', [
'registration_number' => 'AB12CDE',
'contact_data' => ['name' => 'John Doe'],
], [
'Authorization' => "Bearer {$this->token}",
'Origin' => 'https://malicious-site.com',
])->assertForbidden();
});

View File

@@ -0,0 +1,250 @@
<?php
use App\Enums\ResponseStatus;
use App\Models\VehicleDataSource;
use App\Models\VehicleRecord;
use App\Models\Website;
use App\Services\DvlaService;
use Illuminate\Support\Facades\Http;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\postJson;
beforeEach(function (): void {
$tier = \App\Models\Tier::factory()->create([
'allowed_fields' => ['registrationNumber', 'make', 'colour'],
]);
$this->website = Website::factory()->create([
'domain' => 'example.com',
'tier_id' => $tier->id,
'cache_hit_rate_limit' => 100,
'external_api_rate_limit' => 10,
'is_active' => true,
]);
$this->token = $this->website->createToken('test')->plainTextToken;
$this->headers = [
'Authorization' => "Bearer {$this->token}",
'Origin' => 'https://example.com',
];
});
test('requires authentication', function (): void {
postJson('/api/vehicle-enquiry', [
'registration_number' => 'ABC123',
])->assertUnauthorized();
});
test('requires registration number', function (): void {
postJson('/api/vehicle-enquiry', [], $this->headers)
->assertUnprocessable()
->assertJsonValidationErrors(['registration_number']);
});
test('returns cached vehicle data when found', function (): void {
$vehicle = VehicleRecord::create([
'registration_number' => 'ABC123',
'data' => [
'registrationNumber' => 'ABC123',
'make' => 'ROVER',
'colour' => 'BLUE',
'fuelType' => 'PETROL',
],
]);
$response = postJson('/api/vehicle-enquiry', [
'registration_number' => 'abc123',
], $this->headers);
$response->assertSuccessful()
->assertJson([
'success' => true,
'data' => [
'registrationNumber' => 'ABC123',
'make' => 'ROVER',
'colour' => 'BLUE',
],
]);
assertDatabaseHas('api_requests', [
'website_id' => $this->website->id,
'registration_number' => 'ABC123',
'response_status' => ResponseStatus::CacheHit->value,
]);
});
test('filters response by tier fields', function (): void {
VehicleRecord::create([
'registration_number' => 'ABC123',
'data' => [
'registrationNumber' => 'ABC123',
'make' => 'ROVER',
'colour' => 'BLUE',
'fuelType' => 'PETROL',
'engineCapacity' => 2494,
],
]);
$response = postJson('/api/vehicle-enquiry', [
'registration_number' => 'ABC123',
], $this->headers);
$response->assertSuccessful()
->assertJson([
'success' => true,
'data' => [
'registrationNumber' => 'ABC123',
'make' => 'ROVER',
'colour' => 'BLUE',
],
])->assertJsonMissing(['fuelType', 'engineCapacity']);
});
test('fetches from dvla when not cached', function (): void {
Http::fake([
'driver-vehicle-licensing.api.gov.uk/*' => Http::response([
'registrationNumber' => 'XYZ789',
'make' => 'FORD',
'colour' => 'RED',
'fuelType' => 'DIESEL',
], 200),
]);
$response = postJson('/api/vehicle-enquiry', [
'registration_number' => 'XYZ789',
'contact_data' => [
'name' => 'John Doe',
'email' => 'john@example.com',
],
], $this->headers);
$response->assertSuccessful()
->assertJson([
'success' => true,
'data' => [
'registrationNumber' => 'XYZ789',
'make' => 'FORD',
'colour' => 'RED',
],
]);
assertDatabaseHas('vehicle_records', [
'registration_number' => 'XYZ789',
]);
assertDatabaseHas('vehicle_data_sources', [
'source_name' => 'dvla',
]);
assertDatabaseHas('api_requests', [
'website_id' => $this->website->id,
'registration_number' => 'XYZ789',
'response_status' => ResponseStatus::ApiFetched->value,
]);
});
test('returns 404 when vehicle not found in dvla', function (): void {
Http::fake([
'driver-vehicle-licensing.api.gov.uk/*' => Http::response([], 404),
]);
$response = postJson('/api/vehicle-enquiry', [
'registration_number' => 'ZZ99ZZZ',
], $this->headers);
$response->assertNotFound()
->assertJson([
'message' => 'Vehicle not found',
]);
assertDatabaseHas('api_requests', [
'response_status' => ResponseStatus::NotFound->value,
]);
});
test('respects cache hit rate limit', function (): void {
$this->website->update(['cache_hit_rate_limit' => 2]);
VehicleRecord::create([
'registration_number' => 'ABC123',
'data' => ['registrationNumber' => 'ABC123', 'make' => 'ROVER'],
]);
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], $this->headers)->assertSuccessful();
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], $this->headers)->assertSuccessful();
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], $this->headers)->assertStatus(429)
->assertJson([
'message' => 'Rate limit exceeded',
]);
});
test('respects external api rate limit', function (): void {
$this->website->update(['external_api_rate_limit' => 1]);
Http::fake([
'driver-vehicle-licensing.api.gov.uk/*' => Http::response([
'registrationNumber' => 'XYZ789',
'make' => 'FORD',
], 200),
]);
postJson('/api/vehicle-enquiry', ['registration_number' => 'NEW1'], $this->headers)->assertSuccessful();
postJson('/api/vehicle-enquiry', ['registration_number' => 'NEW2'], $this->headers)->assertStatus(429)
->assertJson([
'message' => 'External API rate limit exceeded',
]);
});
test('inactive website cannot make requests', function (): void {
$this->website->update(['is_active' => false]);
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], $this->headers)->assertForbidden()
->assertJson([
'message' => 'Account inactive',
]);
});
test('rejects request when origin does not match website domain', function (): void {
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], [
'Authorization' => "Bearer {$this->token}",
'Origin' => 'https://malicious-site.com',
])->assertForbidden()
->assertJson(['message' => 'Forbidden']);
});
test('rejects request when no origin or referer header is present', function (): void {
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], [
'Authorization' => "Bearer {$this->token}",
])->assertForbidden()
->assertJson(['message' => 'Forbidden']);
});
test('accepts request with referer header matching website domain', function (): void {
VehicleRecord::create([
'registration_number' => 'ABC123',
'data' => ['registrationNumber' => 'ABC123', 'make' => 'ROVER', 'colour' => 'BLUE'],
]);
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], [
'Authorization' => "Bearer {$this->token}",
'Referer' => 'https://example.com/some/page',
])->assertSuccessful();
});
test('bypass_rate_limit website skips origin validation', function (): void {
$this->website->update(['bypass_rate_limit' => true]);
VehicleRecord::create([
'registration_number' => 'ABC123',
'data' => ['registrationNumber' => 'ABC123', 'make' => 'ROVER', 'colour' => 'BLUE'],
]);
postJson('/api/vehicle-enquiry', ['registration_number' => 'ABC123'], [
'Authorization' => "Bearer {$this->token}",
])->assertSuccessful();
});

47
tests/Pest.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/
pest()->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
function something()
{
// ..
}

10
tests/TestCase.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}

View File

@@ -0,0 +1,5 @@
<?php
test('that true is true', function () {
expect(true)->toBeTrue();
});