Replace the bespoke HashIdTrait with a Laravel-standard HasNanoIds trait built on HasUniqueStringIds, mirroring HasUuids/HasUlids. Generation and validation go through new Str::nanoid()/Str::isNanoid() macros (wrapping hidehalo/nanoid), with the alphabet/length co-located on App\Contracts\Model. - Apply HasNanoIds to Pirep, Flight, Acars, File; drop manual $keyType and the now-redundant #[WithoutIncrementing] attribute - Deprecate Utils::generateNewId() as a shim over Str::nanoid() and migrate callers (FlightImporter, Maintenance, RouteForgeService, FileService, VMSAcars) - Remove HashIdTrait - Factories generate nanoid ids so test data matches production - Fix FlightService::getAccessibleFlightIds() casting nanoid flight ids to int (0), which broke flight search under rank/typerating restrictions, and drop matching (int) casts from the affected test assertions
553 lines
18 KiB
PHP
553 lines
18 KiB
PHP
<?php
|
|
|
|
use App\Enums\UserState;
|
|
use App\Events\ProfileUpdated;
|
|
use App\Exceptions\PilotIdNotFound;
|
|
use App\Exceptions\UserPilotIdExists;
|
|
use App\Models\Airline;
|
|
use App\Models\Airport;
|
|
use App\Models\Fare;
|
|
use App\Models\Flight;
|
|
use App\Models\Pirep;
|
|
use App\Models\Rank;
|
|
use App\Models\Role;
|
|
use App\Models\Subfleet;
|
|
use App\Models\User;
|
|
use App\Services\FareService;
|
|
use App\Services\UserService;
|
|
use App\Widgets\LatestPilots;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
test('rank subfleets', function (): void {
|
|
// Add subfleets and aircraft, but also add another
|
|
// set of subfleets
|
|
$subfleet = Subfleet::factory()->hasAircraft(2)->count(2)->create();
|
|
$subfleetA = $subfleet->first();
|
|
|
|
$rank = Rank::factory()->hasAttached($subfleetA)->create();
|
|
|
|
$user = User::factory()->create([
|
|
'rank_id' => $rank->id,
|
|
]);
|
|
|
|
$added_aircraft = $subfleetA->aircraft->pluck('id');
|
|
|
|
$subfleets = $user->allowedSubfleets()->with('aircraft')->get();
|
|
expect($subfleets->count())->toEqual(1);
|
|
|
|
$subfleet = $subfleets[0];
|
|
$all_aircraft = $subfleet->aircraft->pluck('id');
|
|
expect($all_aircraft)->toEqual($added_aircraft);
|
|
|
|
/**
|
|
* Check via API
|
|
*/
|
|
apiAs($user);
|
|
|
|
$resp = $this->get('/api/user/fleet', [])->assertStatus(200);
|
|
$body = $resp->json()['data'];
|
|
|
|
// Get the subfleet that's been added in
|
|
$subfleet_from_api = $body[0];
|
|
expect($subfleet_from_api['id'])->toEqual($subfleet->id);
|
|
|
|
// Get all the aircraft from that subfleet
|
|
$aircraft_from_api = collect($subfleet_from_api['aircraft'])->pluck('id');
|
|
expect($aircraft_from_api)->toEqual($added_aircraft);
|
|
|
|
/**
|
|
* Check the user ID call
|
|
*/
|
|
$resp = $this->get('/api/users/'.$user->id.'/fleet', [])->assertStatus(200);
|
|
$body = $resp->json()['data'];
|
|
|
|
// Get the subfleet that's been added in
|
|
$subfleet_from_api = $body[0];
|
|
expect($subfleet_from_api['id'])->toEqual($subfleet->id);
|
|
|
|
// Get all the aircraft from that subfleet
|
|
$aircraft_from_api = collect($subfleet_from_api['aircraft'])->pluck('id');
|
|
expect($aircraft_from_api)->toEqual($added_aircraft);
|
|
});
|
|
|
|
test('get all aircraft', function (): void {
|
|
$fareSvc = app(FareService::class);
|
|
|
|
// Add subfleets and aircraft, but also add another
|
|
// set of subfleets
|
|
$subfleetA = Subfleet::factory()->hasAircraft(2)->create();
|
|
$subfleetB = Subfleet::factory()->hasAircraft(2)->create();
|
|
|
|
$fare = Fare::factory()->create([
|
|
'price' => 20,
|
|
'capacity' => 200,
|
|
]);
|
|
|
|
$overrides = [
|
|
'price' => 50,
|
|
'capacity' => 400,
|
|
];
|
|
|
|
$fareSvc->setForSubfleet($subfleetA, $fare, $overrides);
|
|
|
|
$added_aircraft = array_merge(
|
|
$subfleetA->aircraft->pluck('id')->toArray(),
|
|
$subfleetB->aircraft->pluck('id')->toArray()
|
|
);
|
|
|
|
$rank = Rank::factory()->hasAttached($subfleetA)->create();
|
|
|
|
$user = User::factory()->create([
|
|
'rank_id' => $rank->id,
|
|
]);
|
|
|
|
apiAs($user);
|
|
|
|
updateSetting('pireps.restrict_aircraft_to_rank', false);
|
|
|
|
$subfleets = $user->allowedSubfleets()->with(['aircraft', 'fares'])->get();
|
|
expect($subfleets->count())->toEqual(2);
|
|
|
|
$all_aircraft = array_merge(
|
|
$subfleets[0]->aircraft->pluck('id')->toArray(),
|
|
$subfleets[1]->aircraft->pluck('id')->toArray()
|
|
);
|
|
|
|
expect($all_aircraft)->toEqual($added_aircraft);
|
|
|
|
// Override resolution now happens at the API Resource boundary (not in
|
|
// the loader). The /api/user/fleet check below pins that surface.
|
|
$subfleetACalled = $fareSvc->getForSubfleet($subfleets->firstWhere('id', $subfleetA->id));
|
|
expect($overrides['price'])->toEqual($subfleetACalled[0]->price)
|
|
->and($overrides['capacity'])->toEqual($subfleetACalled[0]->capacity);
|
|
|
|
/**
|
|
* Check via API, but should only show the single subfleet being returned
|
|
*/
|
|
updateSetting('pireps.restrict_aircraft_to_rank', true);
|
|
|
|
$resp = $this->get('/api/user/fleet', [], $user)->assertStatus(200);
|
|
|
|
// Get all the aircraft from that subfleet, check the fares
|
|
$body = $resp->json()['data'];
|
|
$subfleetAFromApi = collect($body)->firstWhere('id', $subfleetA->id);
|
|
expect($overrides['price'])->toEqual($subfleetAFromApi['fares'][0]['price'])
|
|
->and($overrides['capacity'])->toEqual($subfleetAFromApi['fares'][0]['capacity']);
|
|
});
|
|
|
|
test('get aircraft allowed from flight', function (): void {
|
|
// Add subfleets and aircraft, but also add another
|
|
// set of subfleets
|
|
$airport = Airport::factory()->create();
|
|
|
|
$subfleetA = Subfleet::factory()->hasAircraft(2, ['airport_id' => $airport->id])->create();
|
|
$subfleetB = Subfleet::factory()->hasAircraft(2)->create();
|
|
|
|
$rank = Rank::factory()->hasAttached($subfleetA)->create();
|
|
$user = User::factory()->create([
|
|
'curr_airport_id' => $airport->id,
|
|
'rank_id' => $rank->id,
|
|
]);
|
|
|
|
apiAs($user);
|
|
|
|
$flight = Flight::factory()->create([
|
|
'airline_id' => $user->airline_id,
|
|
'dpt_airport_id' => $airport->id,
|
|
]);
|
|
|
|
$flight->subfleets()->syncWithoutDetaching([
|
|
$subfleetA->id,
|
|
$subfleetB->id,
|
|
]);
|
|
|
|
// Make sure no flights are filtered out
|
|
updateSetting('pilots.only_flights_from_current', false);
|
|
|
|
// And restrict the aircraft
|
|
updateSetting('pireps.restrict_aircraft_to_rank', false);
|
|
|
|
$response = $this->get('/api/flights/'.$flight->id, [], $user);
|
|
$response->assertStatus(200);
|
|
|
|
expect($response->json()['data']['subfleets'])->toHaveCount(2);
|
|
|
|
/*
|
|
* Now make sure it's filtered out
|
|
*/
|
|
updateSetting('pireps.restrict_aircraft_to_rank', true);
|
|
|
|
/**
|
|
* Make sure it's filtered out from the single flight call
|
|
*/
|
|
$response = $this->get('/api/flights/'.$flight->id, [], $user);
|
|
$response->assertStatus(200);
|
|
|
|
expect($response->json()['data']['subfleets'])->toHaveCount(1);
|
|
|
|
/**
|
|
* Make sure it's filtered out from the flight list
|
|
*/
|
|
$response = $this->get('/api/flights', [], $user);
|
|
$body = $response->json()['data'];
|
|
$response->assertStatus(200);
|
|
expect($body[0]['subfleets'])->toHaveCount(1);
|
|
|
|
/**
|
|
* Filtered from search?
|
|
*/
|
|
$response = $this->get('/api/flights/search?flight_id='.$flight->id, [], $user);
|
|
$response->assertStatus(200);
|
|
|
|
$body = $response->json()['data'];
|
|
expect($body[0]['subfleets'])->toHaveCount(1);
|
|
});
|
|
|
|
test('api flight list excludes flights without allowable subfleets', function (): void {
|
|
updateSetting('pilots.restrict_to_company', false);
|
|
updateSetting('pilots.only_flights_from_current', false);
|
|
updateSetting('pireps.restrict_aircraft_to_rank', true);
|
|
updateSetting('pireps.restrict_aircraft_to_typerating', false);
|
|
|
|
$airline = Airline::factory()->create();
|
|
$allowedSubfleet = Subfleet::factory()->create(['airline_id' => $airline->id]);
|
|
$restrictedSubfleet = Subfleet::factory()->create(['airline_id' => $airline->id]);
|
|
$rank = Rank::factory()->hasAttached($allowedSubfleet)->create();
|
|
|
|
/** @var User $user */
|
|
$user = User::factory()->create([
|
|
'airline_id' => $airline->id,
|
|
'rank_id' => $rank->id,
|
|
]);
|
|
|
|
apiAs($user);
|
|
|
|
$allowedFlight = Flight::factory()->create([
|
|
'airline_id' => $airline->id,
|
|
]);
|
|
$allowedFlight->subfleets()->syncWithoutDetaching([$allowedSubfleet->id]);
|
|
|
|
$restrictedFlight = Flight::factory()->create([
|
|
'airline_id' => $airline->id,
|
|
]);
|
|
$restrictedFlight->subfleets()->syncWithoutDetaching([$restrictedSubfleet->id]);
|
|
|
|
$openFlight = Flight::factory()->create([
|
|
'airline_id' => $airline->id,
|
|
]);
|
|
|
|
$response = $this->get('/api/flights');
|
|
$response->assertOk();
|
|
|
|
$flightIds = collect($response->json('data'))->pluck('id')->all();
|
|
|
|
expect($flightIds)
|
|
->toContain($allowedFlight->id)
|
|
->toContain($openFlight->id)
|
|
->not->toContain($restrictedFlight->id);
|
|
|
|
$searchResponse = $this->get('/api/flights/search?flight_id='.$restrictedFlight->id);
|
|
$searchResponse->assertOk();
|
|
|
|
expect($searchResponse->json('data'))->toHaveCount(0);
|
|
});
|
|
|
|
test('user pilot id change already exists', function (): void {
|
|
$user1 = User::factory()->create();
|
|
$user2 = User::factory()->create();
|
|
|
|
app(UserService::class)->changePilotId($user1, $user2->pilot_id);
|
|
})->throws(UserPilotIdExists::class);
|
|
|
|
test('user pilot id split', function (): void {
|
|
$userSvc = app(UserService::class);
|
|
|
|
$user = User::factory()->create();
|
|
$found_user = $userSvc->findUserByPilotId($user->ident);
|
|
expect($found_user->id)->toEqual($user->id);
|
|
|
|
// Look for them with the IATA code
|
|
$found_user = $userSvc->findUserByPilotId($user->airline->iata.$user->pilot_id);
|
|
expect($found_user->id)->toEqual($user->id);
|
|
});
|
|
|
|
test('user pilot id split invalid id', function (): void {
|
|
/** @var User $user */
|
|
$user = User::factory()->create();
|
|
|
|
app(UserService::class)->findUserByPilotId($user->airline->iata);
|
|
})->throws(PilotIdNotFound::class);
|
|
|
|
test('user pilot id invalid iata', function (): void {
|
|
$airline = Airline::factory()->create(['icao' => 'ABC', 'iata' => null]);
|
|
$user = User::factory()->create(['airline_id' => $airline->id]);
|
|
|
|
app(UserService::class)->findUserByPilotId('123');
|
|
})->throws(PilotIdNotFound::class);
|
|
|
|
test('user pilot id added', function (): void {
|
|
$userSvc = app(UserService::class);
|
|
|
|
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
|
$new_user['password'] = Hash::make('secret');
|
|
$user = $userSvc->createUser($new_user);
|
|
expect($user->pilot_id)->toBeGreaterThan(0);
|
|
|
|
// Add a second user
|
|
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
|
$new_user['password'] = Hash::make('secret');
|
|
$user2 = $userSvc->createUser($new_user);
|
|
expect($user2->pilot_id)->toBeGreaterThan($user->pilot_id);
|
|
|
|
// Now try to change the original user's pilot_id to 4
|
|
$user = $userSvc->changePilotId($user, 4);
|
|
expect($user->pilot_id)->toEqual(4);
|
|
|
|
// Create a new user and the pilot_id should be greater than the manually set 4
|
|
$user3 = User::factory()->create();
|
|
expect($user3->pilot_id)->toBeGreaterThan(4);
|
|
});
|
|
|
|
test('user pilot deleted', function (): void {
|
|
$userSvc = app(UserService::class);
|
|
|
|
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
|
$new_user['password'] = Hash::make('secret');
|
|
$admin_user = $userSvc->createUser($new_user);
|
|
|
|
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
|
$new_user['password'] = Hash::make('secret');
|
|
$user = $userSvc->createUser($new_user);
|
|
expect($user->pilot_id)->toBeGreaterThan(0);
|
|
|
|
// Delete the user
|
|
$userSvc->removeUser($user);
|
|
|
|
$response = $this->get('/api/user/'.$user->id, [], $admin_user);
|
|
$response->assertStatus(404);
|
|
|
|
// Get from the DB
|
|
$user = User::find($user->id);
|
|
expect($user)->toBeNull();
|
|
});
|
|
|
|
test('user pilot deleted with pireps', function (): void {
|
|
$userSvc = app(UserService::class);
|
|
|
|
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
|
$new_user['password'] = Hash::make('secret');
|
|
$admin_user = $userSvc->createUser($new_user);
|
|
|
|
$new_user = User::factory()->make()->makeVisible(['api_key', 'name', 'email'])->toArray();
|
|
$new_user['password'] = Hash::make('secret');
|
|
$user = $userSvc->createUser($new_user);
|
|
expect($user->pilot_id)->toBeGreaterThan(0);
|
|
|
|
Pirep::factory()->create([
|
|
'user_id' => $user->id,
|
|
]);
|
|
|
|
// Delete the user
|
|
$userSvc->removeUser($user);
|
|
|
|
$response = $this->get('/api/user/'.$user->id, [], $admin_user);
|
|
$response->assertStatus(404);
|
|
|
|
// Get from the DB
|
|
$user = User::find($user->id);
|
|
expect($user->name)->toEqual('Deleted User');
|
|
$this->assertNotEquals($new_user['password'], $user->password);
|
|
});
|
|
|
|
test('user name private', function (): void {
|
|
$vals = [
|
|
'Firstname' => 'Firstname',
|
|
'Firstname Lastname' => 'Firstname L',
|
|
'Firstname Middlename Lastname' => 'Firstname Middlename L',
|
|
'First Mid1 mid2 last' => 'First Mid1 Mid2 L',
|
|
];
|
|
|
|
foreach ($vals as $input => $expected) {
|
|
$user = new User(['name' => $input]);
|
|
expect($user->name_private)->toEqual($expected);
|
|
}
|
|
});
|
|
|
|
test('user leave', function (): void {
|
|
$userSvc = app(UserService::class);
|
|
|
|
User::factory()->create([
|
|
'status' => UserState::ACTIVE,
|
|
]);
|
|
|
|
$users_on_leave = $userSvc->findUsersOnLeave();
|
|
expect($users_on_leave)->toHaveCount(0);
|
|
|
|
updateSetting('pilots.auto_leave_days', 1);
|
|
$user = User::factory()->create([
|
|
'state' => UserState::ACTIVE,
|
|
'created_at' => Carbon::now('UTC')->subDays(5),
|
|
]);
|
|
|
|
$users_on_leave = $userSvc->findUsersOnLeave();
|
|
expect($users_on_leave)->toHaveCount(1)
|
|
->and($users_on_leave->first()->id)->toEqual($user->id);
|
|
|
|
// Give that user a new PIREP, still old
|
|
$pirep = Pirep::factory()->create([
|
|
'user_id' => $user->id,
|
|
'created_at' => Carbon::now('UTC')->subDays(5),
|
|
'submitted_at' => Carbon::now('UTC')->subDays(5),
|
|
]);
|
|
|
|
$user->last_pirep_id = $pirep->id;
|
|
$user->save();
|
|
$user->refresh();
|
|
|
|
$users_on_leave = $userSvc->findUsersOnLeave();
|
|
expect($users_on_leave)->toHaveCount(1)
|
|
->and($users_on_leave->first()->id)->toEqual($user->id);
|
|
|
|
// Create a new PIREP
|
|
/** @var Pirep $pirep */
|
|
$pirep = Pirep::factory()->create([
|
|
'user_id' => $user->id,
|
|
'created_at' => Carbon::now('UTC'),
|
|
'submitted_at' => Carbon::now('UTC'),
|
|
]);
|
|
|
|
$user->last_pirep_id = $pirep->id;
|
|
$user->save();
|
|
$user->refresh();
|
|
|
|
$users_on_leave = $userSvc->findUsersOnLeave();
|
|
expect($users_on_leave)->toHaveCount(0);
|
|
|
|
// Check disable_activity_checks
|
|
$user = User::factory()->create([
|
|
'status' => UserState::ACTIVE,
|
|
'created_at' => Carbon::now('UTC')->subDays(5),
|
|
]);
|
|
|
|
$role = Role::factory()->create([
|
|
'disable_activity_checks' => true,
|
|
]);
|
|
|
|
$user->assignRole($role);
|
|
$user->save();
|
|
|
|
$users_on_leave = $userSvc->findUsersOnLeave();
|
|
expect($users_on_leave)->toHaveCount(0);
|
|
});
|
|
|
|
test('event called when profile updated', function (): void {
|
|
Event::fake();
|
|
$user = User::factory()->create();
|
|
|
|
$body = [
|
|
'name' => 'Test User',
|
|
'email' => $user->email,
|
|
'airline_id' => $user->airline_id,
|
|
];
|
|
|
|
$resp = $this->actingAs($user)->put('/profile/'.$user->id, $body);
|
|
|
|
Event::assertDispatched(ProfileUpdated::class);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Characterization tests for the /users page + LatestPilots widget
|
|
|--------------------------------------------------------------------------
|
|
| These tests lock in the CURRENT behavior of the public pilots list and
|
|
| the LatestPilots dashboard widget so that subsequent refactoring (Phase 4
|
|
| Prettus repository removal) can be verified to be behavior-preserving.
|
|
*/
|
|
|
|
test('pilots list renders all states when hide_inactive is off', function (): void {
|
|
updateSetting('pilots.hide_inactive', false);
|
|
|
|
User::factory()->create(['name' => 'PilotActive', 'state' => UserState::ACTIVE]);
|
|
User::factory()->create(['name' => 'PilotPending', 'state' => UserState::PENDING]);
|
|
User::factory()->create(['name' => 'PilotLeave', 'state' => UserState::ON_LEAVE]);
|
|
|
|
$response = $this->get('/users');
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('PilotActive');
|
|
$response->assertSee('PilotPending');
|
|
$response->assertSee('PilotLeave');
|
|
});
|
|
|
|
test('pilots list filters to active when hide_inactive is on', function (): void {
|
|
updateSetting('pilots.hide_inactive', true);
|
|
|
|
User::factory()->create(['name' => 'PilotActive', 'state' => UserState::ACTIVE]);
|
|
User::factory()->create(['name' => 'PilotPending', 'state' => UserState::PENDING]);
|
|
User::factory()->create(['name' => 'PilotRejected', 'state' => UserState::REJECTED]);
|
|
|
|
$response = $this->get('/users');
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('PilotActive');
|
|
$response->assertDontSee('PilotPending');
|
|
$response->assertDontSee('PilotRejected');
|
|
});
|
|
|
|
test('pilots list filters by free-text search', function (): void {
|
|
updateSetting('pilots.hide_inactive', false);
|
|
|
|
User::factory()->create(['name' => 'JohnDoe', 'state' => UserState::ACTIVE]);
|
|
User::factory()->create(['name' => 'JaneSmith', 'state' => UserState::ACTIVE]);
|
|
|
|
$response = $this->get('/users?search=JohnDoe');
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('JohnDoe');
|
|
$response->assertDontSee('JaneSmith');
|
|
});
|
|
|
|
test('pilots list filters by field-specific search syntax', function (): void {
|
|
updateSetting('pilots.hide_inactive', false);
|
|
|
|
User::factory()->create(['name' => 'JohnDoe', 'state' => UserState::ACTIVE]);
|
|
User::factory()->create(['name' => 'BobSmith', 'state' => UserState::ACTIVE]);
|
|
|
|
$response = $this->get('/users?search=name:JohnDoe');
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('JohnDoe');
|
|
$response->assertDontSee('BobSmith');
|
|
});
|
|
|
|
test('LatestPilots widget excludes deleted users and orders by created_at desc', function (): void {
|
|
User::factory()->create([
|
|
'name' => 'Newest',
|
|
'state' => UserState::ACTIVE,
|
|
'created_at' => Carbon::now('UTC'),
|
|
]);
|
|
User::factory()->create([
|
|
'name' => 'Older',
|
|
'state' => UserState::ACTIVE,
|
|
'created_at' => Carbon::now('UTC')->subDay(),
|
|
]);
|
|
User::factory()->create([
|
|
'name' => 'Deleted',
|
|
'state' => UserState::DELETED,
|
|
'created_at' => Carbon::now('UTC')->addHour(),
|
|
]);
|
|
|
|
$widget = new LatestPilots(['count' => 5]);
|
|
$rendered = (string) $widget->run()->render();
|
|
|
|
expect($rendered)->toContain('Newest');
|
|
expect($rendered)->toContain('Older');
|
|
expect($rendered)->not->toContain('Deleted');
|
|
|
|
$newestPos = strpos($rendered, 'Newest');
|
|
$olderPos = strpos($rendered, 'Older');
|
|
expect($newestPos)->toBeLessThan($olderPos);
|
|
});
|