919 lines
36 KiB
PHP
919 lines
36 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
usesApiSuite();
|
|
|
|
const SUBUSER_PASSWORD_POLICY_MESSAGE = 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
|
|
|
|
it('lists subusers when an existing grant has legacy zero permissions', function (): void {
|
|
api_test_covers('GET /subusers', 'happy');
|
|
|
|
$session = api_fixtures()->createUserSession(['list_own_subusers']);
|
|
$subuser = api_fixtures()->createSubuser([
|
|
'name' => 'Legacy Permission Driver',
|
|
]);
|
|
$grantId = api_fixtures()->grantSubuser(
|
|
$subuser['id'],
|
|
$session['user']['customer_number'],
|
|
['VEHICLES_LIST']
|
|
);
|
|
|
|
$legacyPermissions = '0';
|
|
$statement = api_test_runtime()->db()->prepare(
|
|
'UPDATE `subuser_grants` SET `permissions` = ? WHERE `id` = ?'
|
|
);
|
|
$statement->bind_param('si', $legacyPermissions, $grantId);
|
|
$statement->execute();
|
|
$statement->close();
|
|
|
|
$response = api_client()->get('/subusers?page=1&limit=5&include_non_enabled=true', $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$matchingSubusers = array_values(array_filter(
|
|
is_array($response->data()) ? $response->data() : [],
|
|
static fn (mixed $item): bool => is_array($item) && (int)($item['id'] ?? 0) === (int)$subuser['id']
|
|
));
|
|
|
|
expect($matchingSubusers)->toHaveCount(1);
|
|
expect($matchingSubusers[0]['grant_permissions'] ?? null)->toBe([]);
|
|
expect($matchingSubusers[0]['permissions'] ?? null)->toBe([]);
|
|
});
|
|
|
|
it('enforces the password policy for setup without rejecting legacy valid passwords at login', function (): void {
|
|
api_test_covers('POST /subusers/setup', 'failure');
|
|
api_test_covers('POST /subusers/auth/password', 'happy');
|
|
|
|
$subuser = api_fixtures()->createSubuser([
|
|
'password_plaintext' => 'invalidpassword',
|
|
]);
|
|
|
|
$setupResponse = api_client()->post('/subusers/setup', [
|
|
'token' => 'policy-test-token',
|
|
'name' => 'Policy Driver',
|
|
'password' => 'invalidpassword',
|
|
]);
|
|
|
|
$authResponse = api_client()->post('/subusers/auth/password', [
|
|
'subuser_id' => $subuser['id'],
|
|
'password' => 'invalidpassword',
|
|
]);
|
|
|
|
$setupResponse
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage(SUBUSER_PASSWORD_POLICY_MESSAGE);
|
|
|
|
$authResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($authResponse->data()['session'] ?? null)->toBeString();
|
|
(new \objects\subusers_o())->invalidateSessionToken((string)$authResponse->data()['session']);
|
|
});
|
|
|
|
it('returns a generic error for invalid subuser credentials', function (): void {
|
|
api_test_covers('POST /subusers/auth/password', 'failure');
|
|
|
|
$response = api_client()->post('/subusers/auth/password', [
|
|
'subuser_id' => 999999999,
|
|
'password' => 'whatever',
|
|
]);
|
|
|
|
$response
|
|
->assertStatus(401)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Invalid credentials');
|
|
});
|
|
|
|
it('lets drivers verify their email with a one-time code', function (): void {
|
|
api_test_covers('GET /subusers/me/verification', 'happy');
|
|
api_test_covers('POST /subusers/me/verification/{channel}/send', 'happy');
|
|
api_test_covers('POST /subusers/me/verification/{channel}/verify', 'happy');
|
|
api_test_covers('POST /subusers/me/verification/{channel}/verify', 'failure');
|
|
|
|
$previousFakeMode = getenv('EMAIL_FAKE_MODE');
|
|
$previousFakePath = getenv('EMAIL_FAKE_DELIVERIES_PATH');
|
|
$fakePath = sys_get_temp_dir() . '/truckwash-subuser-verification-' . bin2hex(random_bytes(6)) . '.jsonl';
|
|
|
|
putenv('EMAIL_FAKE_MODE=1');
|
|
putenv('EMAIL_FAKE_DELIVERIES_PATH=' . $fakePath);
|
|
\classes\email::resetFakeDeliveries();
|
|
api_fixtures()->setModuleConfig('Email', 'mailersend_enabled', 'true', 'bool');
|
|
api_test_runtime()->restartServer();
|
|
|
|
try {
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Verification Customer']);
|
|
$session = api_fixtures()->createSubuserSession(
|
|
(int)$customer['customer_number'],
|
|
['VEHICLES_LIST'],
|
|
[
|
|
'name' => 'Verification Driver',
|
|
'email' => 'driver.verify@example.test',
|
|
'email_verified_at' => null,
|
|
'phone_verified_at' => null,
|
|
]
|
|
);
|
|
|
|
$status = api_client()->get('/subusers/me/verification', $session['headers']);
|
|
$send = api_client()->post('/subusers/me/verification/email/send', [], $session['headers']);
|
|
|
|
$status
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
$send
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($status->data()['email']['verified'] ?? null)->toBeFalse();
|
|
expect($send->data()['delivery']['status'] ?? null)->toBe('sent');
|
|
expect($send->data()['verification']['email']['verified'] ?? null)->toBeFalse();
|
|
|
|
\classes\email::syncFakeDeliveries();
|
|
$deliveries = \classes\email::$fake_deliveries;
|
|
expect($deliveries)->toHaveCount(1);
|
|
$message = (string)($deliveries[0]['message'] ?? '');
|
|
preg_match('/\b([0-9]{6})\b/', $message, $matches);
|
|
expect($matches[1] ?? null)->toBeString();
|
|
|
|
$wrong = api_client()->post('/subusers/me/verification/email/verify', [
|
|
'code' => '000000',
|
|
], $session['headers']);
|
|
$wrong
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
|
|
$verify = api_client()->post('/subusers/me/verification/email/verify', [
|
|
'code' => (string)$matches[1],
|
|
], $session['headers']);
|
|
$verify
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($verify->data()['verification']['email']['verified'] ?? null)->toBeTrue();
|
|
expect($verify->data()['subuser']['email_verified'] ?? null)->toBeTrue();
|
|
|
|
$row = api_fixtures()->fetchRowById('subusers', (int)$session['subuser']['id']);
|
|
expect($row['email_verified_at'] ?? null)->not->toBeNull();
|
|
expect($row['phone_verified_at'] ?? null)->toBeNull();
|
|
} finally {
|
|
api_test_runtime()->restartServer();
|
|
\classes\email::resetFakeDeliveries();
|
|
if ($previousFakeMode === false) {
|
|
putenv('EMAIL_FAKE_MODE');
|
|
} else {
|
|
putenv('EMAIL_FAKE_MODE=' . $previousFakeMode);
|
|
}
|
|
if ($previousFakePath === false) {
|
|
putenv('EMAIL_FAKE_DELIVERIES_PATH');
|
|
} else {
|
|
putenv('EMAIL_FAKE_DELIVERIES_PATH=' . $previousFakePath);
|
|
}
|
|
if (is_file($fakePath)) {
|
|
unlink($fakePath);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('clears email verification when a driver changes their email', function (): void {
|
|
api_test_covers('PUT /subusers/me', 'happy');
|
|
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Driver Email Change Customer']);
|
|
$session = api_fixtures()->createSubuserSession(
|
|
(int)$customer['customer_number'],
|
|
['VEHICLES_LIST'],
|
|
[
|
|
'email' => 'old.driver.email@example.test',
|
|
'email_verified_at' => '2026-07-13 10:00:00',
|
|
'phone_verified_at' => '2026-07-13 10:00:00',
|
|
]
|
|
);
|
|
|
|
$response = api_client()->request('PUT', '/subusers/me', [
|
|
'email' => 'new.driver.email@example.test',
|
|
], $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($response->data()['email'] ?? null)->toBe('new.driver.email@example.test');
|
|
expect($response->data()['email_verified'] ?? null)->toBeFalse();
|
|
expect($response->data()['phone_verified'] ?? null)->toBeTrue();
|
|
|
|
$row = api_fixtures()->fetchRowById('subusers', (int)$session['subuser']['id']);
|
|
expect($row['email_verified_at'] ?? null)->toBeNull();
|
|
expect($row['phone_verified_at'] ?? null)->not->toBeNull();
|
|
});
|
|
|
|
it('requires subuser management access before exposing permission nodes', function (): void {
|
|
api_test_covers('GET /subusers/permission-nodes', 'auth');
|
|
|
|
$unauthenticated = api_client()->get('/subusers/permission-nodes');
|
|
$unauthenticated
|
|
->assertStatus(401)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
|
|
$session = api_fixtures()->createUserSession(['list_own_subusers']);
|
|
$authorized = api_client()->get('/subusers/permission-nodes', $session['headers']);
|
|
$authorized
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($authorized->data())->toBeArray()->not->toBeEmpty();
|
|
});
|
|
|
|
it('requires subuser management access before exposing simplified permission templates', function (): void {
|
|
api_test_covers('GET /subusers/permission-templates', 'auth');
|
|
api_test_covers('GET /subusers/permission-templates', 'happy');
|
|
|
|
$unauthenticated = api_client()->get('/subusers/permission-templates');
|
|
$unauthenticated
|
|
->assertStatus(401)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
|
|
$session = api_fixtures()->createUserSession(['list_own_subusers']);
|
|
$authorized = api_client()->get('/subusers/permission-templates', $session['headers']);
|
|
$authorized
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($authorized->data()['templates'] ?? null)->toBeArray()->not->toBeEmpty();
|
|
expect(array_column($authorized->data()['templates'], 'key'))
|
|
->toContain('driver')
|
|
->toContain('booking_coordinator')
|
|
->toContain('fleet_admin')
|
|
->toContain('deactivated');
|
|
expect($authorized->data()['groups'] ?? null)->toBeArray()->not->toBeEmpty();
|
|
});
|
|
|
|
it('rejects customer subuser listing without own-scope permission', function (): void {
|
|
api_test_covers('GET /subusers', 'auth');
|
|
|
|
$session = api_fixtures()->createUserSession(['user']);
|
|
$response = api_client()->get('/subusers?page=1&limit=5', $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
});
|
|
|
|
it('replaces older setup tokens when a new setup token is generated', function (): void {
|
|
$subuser = api_fixtures()->createSubuser([
|
|
'password_plaintext' => null,
|
|
'name' => 'Pending Setup Driver',
|
|
]);
|
|
$subuserObject = (new \objects\subusers_o())->select((int)$subuser['id']);
|
|
$subuserObject->getObjectProperties();
|
|
|
|
$firstToken = $subuserObject->generateSetupToken();
|
|
$secondToken = $subuserObject->generateSetupToken();
|
|
|
|
try {
|
|
expect((new \objects\subusers_o())->getSubuserBySetupToken($firstToken))->toBeNull();
|
|
expect((new \objects\subusers_o())->getSubuserBySetupToken($secondToken))->not->toBeNull();
|
|
} finally {
|
|
(new \objects\subusers_o())->invalidateSetupToken($secondToken);
|
|
}
|
|
});
|
|
|
|
it('validates setup identifiers before setting the driver password', function (): void {
|
|
api_test_covers('POST /subusers/setup', 'failure');
|
|
|
|
$existing = api_fixtures()->createSubuser([
|
|
'username' => 'existing-driver-setup',
|
|
]);
|
|
$pending = api_fixtures()->createSubuser([
|
|
'password_plaintext' => null,
|
|
'username' => 'pending-driver-setup',
|
|
'name' => 'Pending Driver',
|
|
]);
|
|
$pendingObject = (new \objects\subusers_o())->select((int)$pending['id']);
|
|
$pendingObject->getObjectProperties();
|
|
$token = $pendingObject->generateSetupToken();
|
|
|
|
try {
|
|
$response = api_client()->post('/subusers/setup', [
|
|
'token' => $token,
|
|
'name' => 'Pending Driver',
|
|
'username' => $existing['username'],
|
|
'password' => 'ValidPass123',
|
|
]);
|
|
|
|
$response
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Account already exists with this username');
|
|
|
|
$row = api_fixtures()->fetchRowById('subusers', (int)$pending['id']);
|
|
expect($row['password'] ?? null)->toBeNull();
|
|
expect((new \objects\subusers_o())->getSubuserBySetupToken($token))->not->toBeNull();
|
|
} finally {
|
|
(new \objects\subusers_o())->invalidateSetupToken($token);
|
|
}
|
|
});
|
|
|
|
it('lists chauffeurs once with grouped grants across customers for superusers', function (): void {
|
|
$session = api_fixtures()->createUserSession(['list_subusers']);
|
|
$firstCustomer = api_fixtures()->createUser([
|
|
'display_name' => 'Fleet Customer Alpha',
|
|
'economic_customer_name' => 'Fleet Customer Alpha',
|
|
]);
|
|
$secondCustomer = api_fixtures()->createUser([
|
|
'display_name' => 'Fleet Customer Beta',
|
|
'economic_customer_name' => 'Fleet Customer Beta',
|
|
]);
|
|
$firstSubuser = api_fixtures()->createSubuser(['name' => 'Alpha Driver']);
|
|
$secondSubuser = api_fixtures()->createSubuser(['name' => 'Beta Driver']);
|
|
$firstVehicle = api_fixtures()->createVehicle([
|
|
'customer_id' => (int)$firstCustomer['customer_number'],
|
|
'type' => 1,
|
|
'reg' => 'ab12345',
|
|
]);
|
|
$firstGrantId = api_fixtures()->grantSubuser(
|
|
(int)$firstSubuser['id'],
|
|
(int)$firstCustomer['customer_number'],
|
|
['VEHICLES_LIST', 'SUBUSERS_LIST', 'SELFSERVE_LIST', 'SELFSERVE_ADD'],
|
|
['assigned_vehicle_id' => (int)$firstVehicle['id']]
|
|
);
|
|
$secondGrantId = api_fixtures()->grantSubuser(
|
|
(int)$secondSubuser['id'],
|
|
(int)$secondCustomer['customer_number'],
|
|
['BOOKINGS_LIST']
|
|
);
|
|
$sharedGrantId = api_fixtures()->grantSubuser(
|
|
(int)$firstSubuser['id'],
|
|
(int)$secondCustomer['customer_number'],
|
|
['ORDERS_LIST']
|
|
);
|
|
|
|
$response = api_client()->get('/superuser/subusers?page=1&limit=20&search=Driver', $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$rows = array_values(array_filter(
|
|
is_array($response->data()) ? $response->data() : [],
|
|
static fn (mixed $item): bool => is_array($item)
|
|
&& in_array((int)($item['id'] ?? 0), [(int)$firstSubuser['id'], (int)$secondSubuser['id']], true)
|
|
));
|
|
|
|
expect($rows)->toHaveCount(2);
|
|
expect($response->meta()['pagination']['total'] ?? null)->toBe(2);
|
|
|
|
$bySubuserId = [];
|
|
foreach ($rows as $row) {
|
|
$bySubuserId[(int)$row['id']] = $row;
|
|
}
|
|
|
|
expect($bySubuserId)->toHaveKeys([(int)$firstSubuser['id'], (int)$secondSubuser['id']]);
|
|
|
|
$firstRow = $bySubuserId[(int)$firstSubuser['id']];
|
|
expect($firstRow['grant_count'] ?? null)->toBe(2);
|
|
expect($firstRow['customer_numbers'] ?? [])->toContain((int)$firstCustomer['customer_number']);
|
|
expect($firstRow['customer_numbers'] ?? [])->toContain((int)$secondCustomer['customer_number']);
|
|
|
|
$firstGrantsById = [];
|
|
foreach ($firstRow['grants'] ?? [] as $grant) {
|
|
$firstGrantsById[(int)$grant['grant_id']] = $grant;
|
|
}
|
|
|
|
expect($firstGrantsById[$firstGrantId]['customer_number'])->toBe((int)$firstCustomer['customer_number']);
|
|
expect($firstGrantsById[$firstGrantId]['assigned_vehicle_id'])->toBe((int)$firstVehicle['id']);
|
|
expect($firstGrantsById[$firstGrantId]['assigned_vehicle_reg'])->toBe('AB12345');
|
|
expect($firstGrantsById[$firstGrantId]['dognvask_enabled'])->toBeTrue();
|
|
expect($firstGrantsById[$sharedGrantId]['customer_number'])->toBe((int)$secondCustomer['customer_number']);
|
|
expect($firstGrantsById[$sharedGrantId]['dognvask_enabled'])->toBeFalse();
|
|
|
|
$secondRow = $bySubuserId[(int)$secondSubuser['id']];
|
|
expect($secondRow['grant_count'] ?? null)->toBe(1);
|
|
expect($secondRow['grants'][0]['grant_id'] ?? null)->toBe($secondGrantId);
|
|
expect($secondRow['grants'][0]['customer_name'] ?? null)->toBe('Fleet Customer Beta');
|
|
});
|
|
|
|
it('paginates superuser chauffeur lists by unique chauffeur instead of grant count', function (): void {
|
|
$session = api_fixtures()->createUserSession(['list_subusers']);
|
|
$suffix = (string)random_int(100000, 999999);
|
|
$firstCustomer = api_fixtures()->createUser(['display_name' => 'Grouped Page Alpha ' . $suffix]);
|
|
$secondCustomer = api_fixtures()->createUser(['display_name' => 'Grouped Page Beta ' . $suffix]);
|
|
$firstSubuser = api_fixtures()->createSubuser(['name' => 'Grouped Page Driver Alpha ' . $suffix]);
|
|
$secondSubuser = api_fixtures()->createSubuser(['name' => 'Grouped Page Driver Beta ' . $suffix]);
|
|
|
|
api_fixtures()->grantSubuser((int)$firstSubuser['id'], (int)$firstCustomer['customer_number'], ['BOOKINGS_LIST']);
|
|
api_fixtures()->grantSubuser((int)$firstSubuser['id'], (int)$secondCustomer['customer_number'], ['ORDERS_LIST']);
|
|
api_fixtures()->grantSubuser((int)$secondSubuser['id'], (int)$secondCustomer['customer_number'], ['VEHICLES_LIST']);
|
|
|
|
$pageOne = api_client()->get(
|
|
'/superuser/subusers?page=1&limit=1&search=' . rawurlencode($suffix) . '&order=name:ASC',
|
|
$session['headers']
|
|
);
|
|
$pageTwo = api_client()->get(
|
|
'/superuser/subusers?page=2&limit=1&search=' . rawurlencode($suffix) . '&order=name:ASC',
|
|
$session['headers']
|
|
);
|
|
|
|
$pageOne
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
$pageTwo
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($pageOne->meta()['pagination']['total'] ?? null)->toBe(2);
|
|
expect($pageTwo->meta()['pagination']['total'] ?? null)->toBe(2);
|
|
expect($pageOne->data())->toHaveCount(1);
|
|
expect($pageTwo->data())->toHaveCount(1);
|
|
expect($pageOne->data()[0]['id'] ?? null)->toBe((int)$firstSubuser['id']);
|
|
expect($pageOne->data()[0]['grant_count'] ?? null)->toBe(2);
|
|
expect($pageTwo->data()[0]['id'] ?? null)->toBe((int)$secondSubuser['id']);
|
|
expect($pageTwo->data()[0]['grant_count'] ?? null)->toBe(1);
|
|
});
|
|
|
|
it('lets superusers invite chauffeurs for a selected customer', function (): void {
|
|
$session = api_fixtures()->createUserSession(['add_subusers']);
|
|
$customer = api_fixtures()->createUser([
|
|
'display_name' => 'Invite Target Customer',
|
|
'economic_customer_name' => 'Invite Target Customer',
|
|
]);
|
|
$phone = 71000000 + ((int)$customer['customer_number'] % 1000000);
|
|
$createdSubuserId = null;
|
|
$createdGrantId = null;
|
|
$setupToken = null;
|
|
|
|
try {
|
|
$response = api_client()->post('/superuser/subusers/invite', [
|
|
'customer_number' => (int)$customer['customer_number'],
|
|
'name' => 'Invited Driver',
|
|
'phone_country_code' => 45,
|
|
'phone' => $phone,
|
|
'permissions' => ['VEHICLES_LIST'],
|
|
'note' => 'Created by superuser test',
|
|
], $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$payload = $response->data();
|
|
$createdSubuserId = isset($payload['subuser']['id']) ? (int)$payload['subuser']['id'] : null;
|
|
$createdGrantId = isset($payload['grant']['id']) ? (int)$payload['grant']['id'] : null;
|
|
$setupToken = isset($payload['invite']['setup_token']) ? (string)$payload['invite']['setup_token'] : null;
|
|
|
|
expect($payload['subuser']['customer_number'] ?? null)->toBe((int)$customer['customer_number']);
|
|
expect($payload['subuser']['name'] ?? null)->toBe('Invited Driver');
|
|
expect($payload['subuser']['grant_permissions'] ?? null)->toBe(['VEHICLES_LIST']);
|
|
expect($payload['grant']['note'] ?? null)->toBe('Created by superuser test');
|
|
expect($payload['invite']['setup_link'] ?? null)->toBeString();
|
|
} finally {
|
|
if ($setupToken !== null && $setupToken !== '') {
|
|
(new \objects\subusers_o())->invalidateSetupToken($setupToken);
|
|
}
|
|
if ($createdGrantId !== null) {
|
|
api_test_runtime()->db()->query('DELETE FROM `subuser_grants` WHERE `id` = ' . $createdGrantId);
|
|
}
|
|
if ($createdSubuserId !== null) {
|
|
api_test_runtime()->db()->query('DELETE FROM `tokens` WHERE `user_id` = ' . $createdSubuserId . " AND `type` = 'AUTH_TOKEN_SUBUSER'");
|
|
api_test_runtime()->db()->query('DELETE FROM `subusers` WHERE `id` = ' . $createdSubuserId);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('applies simplified driver access templates when inviting and updating chauffeur grants', function (): void {
|
|
api_test_covers('POST /superuser/subusers/invite', 'happy');
|
|
api_test_covers('PATCH /superuser/users/{user_id}/subusers/grants/{grant_id}', 'happy');
|
|
api_test_covers('PATCH /superuser/users/{user_id}/subusers/grants/{grant_id}', 'failure');
|
|
|
|
$session = api_fixtures()->createUserSession(['add_subusers', 'manage_subuser_grants']);
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Template Target Customer']);
|
|
$phone = 72000000 + ((int)$customer['customer_number'] % 1000000);
|
|
$createdSubuserId = null;
|
|
$createdGrantId = null;
|
|
$setupToken = null;
|
|
|
|
try {
|
|
$invite = api_client()->post('/superuser/subusers/invite', [
|
|
'customer_number' => (int)$customer['customer_number'],
|
|
'name' => 'Template Driver',
|
|
'phone_country_code' => 45,
|
|
'phone' => $phone,
|
|
'permission_template_key' => 'booking_coordinator',
|
|
], $session['headers']);
|
|
|
|
$invite
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$payload = $invite->data();
|
|
$createdSubuserId = isset($payload['subuser']['id']) ? (int)$payload['subuser']['id'] : null;
|
|
$createdGrantId = isset($payload['grant']['id']) ? (int)$payload['grant']['id'] : null;
|
|
$setupToken = isset($payload['invite']['setup_token']) ? (string)$payload['invite']['setup_token'] : null;
|
|
|
|
expect($payload['subuser']['permission_template_key'] ?? null)->toBe('booking_coordinator');
|
|
expect($payload['subuser']['grant_permissions'] ?? null)
|
|
->toContain('BOOKINGS_EDIT')
|
|
->toContain('BOOKINGS_ADD')
|
|
->not->toContain('SUBUSERS_ADD');
|
|
|
|
$deactivate = api_client()->request(
|
|
'PATCH',
|
|
'/superuser/users/' . $customer['id'] . '/subusers/grants/' . $createdGrantId,
|
|
['permission_template_key' => 'deactivated'],
|
|
$session['headers']
|
|
);
|
|
|
|
$deactivate
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($deactivate->data()['subuser']['permission_template_key'] ?? null)->toBe('deactivated');
|
|
expect($deactivate->data()['grant']['enabled'] ?? null)->toBeFalse();
|
|
expect($deactivate->data()['grant']['permissions'] ?? null)->toBe([]);
|
|
|
|
$invalid = api_client()->request(
|
|
'PATCH',
|
|
'/superuser/users/' . $customer['id'] . '/subusers/grants/' . $createdGrantId,
|
|
['permission_template_key' => 'unknown_template'],
|
|
$session['headers']
|
|
);
|
|
|
|
$invalid
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
} finally {
|
|
if ($setupToken !== null && $setupToken !== '') {
|
|
(new \objects\subusers_o())->invalidateSetupToken($setupToken);
|
|
}
|
|
if ($createdGrantId !== null) {
|
|
api_test_runtime()->db()->query('DELETE FROM `subuser_grants` WHERE `id` = ' . $createdGrantId);
|
|
}
|
|
if ($createdSubuserId !== null) {
|
|
api_test_runtime()->db()->query('DELETE FROM `tokens` WHERE `user_id` = ' . $createdSubuserId . " AND `type` = 'AUTH_TOKEN_SUBUSER'");
|
|
api_test_runtime()->db()->query('DELETE FROM `subusers` WHERE `id` = ' . $createdSubuserId);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('lists and summarizes chauffeurs through the user-scoped superuser route', function (): void {
|
|
$session = api_fixtures()->createUserSession(['list_subusers']);
|
|
$targetCustomer = api_fixtures()->createUser([
|
|
'display_name' => 'Scoped Customer Alpha',
|
|
'economic_customer_name' => 'Scoped Customer Alpha',
|
|
]);
|
|
$otherCustomer = api_fixtures()->createUser([
|
|
'display_name' => 'Scoped Customer Beta',
|
|
'economic_customer_name' => 'Scoped Customer Beta',
|
|
]);
|
|
$activeSubuser = api_fixtures()->createSubuser(['name' => 'Scoped Active Driver']);
|
|
$pendingSubuser = api_fixtures()->createSubuser([
|
|
'name' => 'Scoped Pending Driver',
|
|
'password_plaintext' => null,
|
|
]);
|
|
$disabledSubuser = api_fixtures()->createSubuser(['name' => 'Scoped Disabled Driver']);
|
|
$otherSubuser = api_fixtures()->createSubuser(['name' => 'Scoped Other Driver']);
|
|
|
|
$activeGrantId = api_fixtures()->grantSubuser(
|
|
(int)$activeSubuser['id'],
|
|
(int)$targetCustomer['customer_number'],
|
|
['VEHICLES_LIST']
|
|
);
|
|
api_fixtures()->grantSubuser(
|
|
(int)$pendingSubuser['id'],
|
|
(int)$targetCustomer['customer_number'],
|
|
['BOOKINGS_LIST']
|
|
);
|
|
$disabledGrantId = api_fixtures()->grantSubuser(
|
|
(int)$disabledSubuser['id'],
|
|
(int)$targetCustomer['customer_number'],
|
|
['ORDERS_LIST']
|
|
);
|
|
api_test_runtime()->db()->query('UPDATE `subuser_grants` SET `enabled` = 0 WHERE `id` = ' . $disabledGrantId);
|
|
api_fixtures()->grantSubuser(
|
|
(int)$otherSubuser['id'],
|
|
(int)$otherCustomer['customer_number'],
|
|
['SELFSERVE_LIST']
|
|
);
|
|
|
|
$response = api_client()->get(
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers?page=1&limit=20&include_non_enabled=true',
|
|
$session['headers']
|
|
);
|
|
$summary = api_client()->get(
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers/summary',
|
|
$session['headers']
|
|
);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
$summary
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$rows = is_array($response->data()) ? $response->data() : [];
|
|
$grantIds = array_map(static fn (array $row): int => (int)($row['grant_id'] ?? 0), $rows);
|
|
|
|
expect($grantIds)->toContain($activeGrantId);
|
|
expect($grantIds)->not->toContain(0);
|
|
expect($rows)->toHaveCount(3);
|
|
foreach ($rows as $row) {
|
|
expect($row['customer_number'] ?? null)->toBe((int)$targetCustomer['customer_number']);
|
|
}
|
|
|
|
expect($response->meta()['user_context']['user_id'] ?? null)->toBe((int)$targetCustomer['id']);
|
|
expect($response->meta()['subusers_summary'] ?? null)->toMatchArray([
|
|
'total' => 3,
|
|
'active' => 1,
|
|
'pending_setup' => 1,
|
|
'disabled' => 1,
|
|
]);
|
|
expect($summary->data())->toMatchArray([
|
|
'total' => 3,
|
|
'active' => 1,
|
|
'pending_setup' => 1,
|
|
'disabled' => 1,
|
|
]);
|
|
});
|
|
|
|
it('edits only customer-matching chauffeur grants through the user-scoped route', function (): void {
|
|
$session = api_fixtures()->createUserSession(['manage_subuser_grants']);
|
|
$targetCustomer = api_fixtures()->createUser(['display_name' => 'Scoped Patch Customer']);
|
|
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Scoped Patch Other']);
|
|
$targetSubuser = api_fixtures()->createSubuser(['name' => 'Patch Target Driver']);
|
|
$otherSubuser = api_fixtures()->createSubuser(['name' => 'Patch Other Driver']);
|
|
$targetVehicle = api_fixtures()->createVehicle([
|
|
'customer_id' => (int)$targetCustomer['customer_number'],
|
|
'type' => 1,
|
|
'reg' => 'scope123',
|
|
]);
|
|
$otherVehicle = api_fixtures()->createVehicle([
|
|
'customer_id' => (int)$otherCustomer['customer_number'],
|
|
'type' => 1,
|
|
'reg' => 'other123',
|
|
]);
|
|
$targetGrantId = api_fixtures()->grantSubuser(
|
|
(int)$targetSubuser['id'],
|
|
(int)$targetCustomer['customer_number'],
|
|
['VEHICLES_LIST']
|
|
);
|
|
$otherGrantId = api_fixtures()->grantSubuser(
|
|
(int)$otherSubuser['id'],
|
|
(int)$otherCustomer['customer_number'],
|
|
['BOOKINGS_LIST']
|
|
);
|
|
|
|
$update = api_client()->request(
|
|
'PATCH',
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers/grants/' . $targetGrantId,
|
|
[
|
|
'enabled' => false,
|
|
'note' => 'Scoped note',
|
|
'permissions' => ['ORDERS_LIST'],
|
|
'assigned_vehicle_id' => (int)$targetVehicle['id'],
|
|
],
|
|
$session['headers']
|
|
);
|
|
$mismatchedVehicle = api_client()->request(
|
|
'PATCH',
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers/grants/' . $targetGrantId,
|
|
['assigned_vehicle_id' => (int)$otherVehicle['id']],
|
|
$session['headers']
|
|
);
|
|
$crossCustomer = api_client()->request(
|
|
'PATCH',
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers/grants/' . $otherGrantId,
|
|
['note' => 'Should not save'],
|
|
$session['headers']
|
|
);
|
|
|
|
$update
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
$mismatchedVehicle
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
$crossCustomer
|
|
->assertStatus(404)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
|
|
expect($update->data()['grant']['enabled'] ?? null)->toBeFalse();
|
|
expect($update->data()['grant']['note'] ?? null)->toBe('Scoped note');
|
|
expect($update->data()['grant']['permissions'] ?? null)->toBe(['ORDERS_LIST']);
|
|
expect($update->data()['grant']['assigned_vehicle_id'] ?? null)->toBe((int)$targetVehicle['id']);
|
|
expect($update->data()['subuser']['assigned_vehicle_reg'] ?? null)->toBe('SCOPE123');
|
|
});
|
|
|
|
it('lets superusers edit chauffeur account details and set passwords', function (): void {
|
|
api_test_covers('PATCH /superuser/subusers/{subuser_id}', 'happy');
|
|
api_test_covers('POST /superuser/subusers/{subuser_id}/password', 'happy');
|
|
|
|
$session = api_fixtures()->createUserSession(['edit_subusers']);
|
|
$subuser = api_fixtures()->createSubuser([
|
|
'password_plaintext' => null,
|
|
'name' => 'Admin Managed Driver',
|
|
'email' => null,
|
|
'phone_country_code' => 45,
|
|
'phone' => 73123456,
|
|
'email_verified_at' => '2026-07-13 10:00:00',
|
|
'phone_verified_at' => '2026-07-13 10:00:00',
|
|
]);
|
|
$subuserObject = (new \objects\subusers_o())->select((int)$subuser['id']);
|
|
$subuserObject->getObjectProperties();
|
|
$setupToken = $subuserObject->generateSetupToken();
|
|
|
|
try {
|
|
$profile = api_client()->request(
|
|
'PATCH',
|
|
'/superuser/subusers/' . $subuser['id'],
|
|
[
|
|
'name' => 'Admin Updated Driver',
|
|
'email' => 'admin.updated.driver@example.com',
|
|
'phone_country_code' => 46,
|
|
'phone' => 73123457,
|
|
],
|
|
$session['headers']
|
|
);
|
|
|
|
$password = api_client()->post(
|
|
'/superuser/subusers/' . $subuser['id'] . '/password',
|
|
['password' => 'ValidPass123'],
|
|
$session['headers']
|
|
);
|
|
|
|
$profile
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
$password
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($profile->data()['subuser']['name'] ?? null)->toBe('Admin Updated Driver');
|
|
expect($profile->data()['subuser']['email'] ?? null)->toBe('admin.updated.driver@example.com');
|
|
expect($profile->data()['subuser']['phone_country_code'] ?? null)->toBe(46);
|
|
expect($profile->data()['subuser']['phone'] ?? null)->toBe(73123457);
|
|
expect($profile->data()['subuser']['email_verified'] ?? null)->toBeFalse();
|
|
expect($profile->data()['subuser']['phone_verified'] ?? null)->toBeFalse();
|
|
expect($password->data()['subuser']['setup_required'] ?? null)->toBeFalse();
|
|
expect((new \objects\subusers_o())->getSubuserBySetupToken($setupToken))->toBeNull();
|
|
|
|
$row = api_fixtures()->fetchRowById('subusers', (int)$subuser['id']);
|
|
expect(password_verify('ValidPass123', (string)($row['password'] ?? '')))->toBeTrue();
|
|
expect($row['email_verified_at'] ?? null)->toBeNull();
|
|
expect($row['phone_verified_at'] ?? null)->toBeNull();
|
|
} finally {
|
|
(new \objects\subusers_o())->invalidateSetupToken($setupToken);
|
|
}
|
|
});
|
|
|
|
it('creates direct chauffeur login links scoped to a selected customer grant', function (): void {
|
|
api_test_covers('POST /superuser/subusers/{subuser_id}/login-link', 'happy');
|
|
api_test_covers('POST /superuser/subusers/{subuser_id}/login-link', 'failure');
|
|
|
|
$session = api_fixtures()->createUserSession(['edit_subusers', 'SUPERUSER_INTIMIDATE']);
|
|
$firstCustomer = api_fixtures()->createUser(['display_name' => 'Direct Login Customer Alpha']);
|
|
$secondCustomer = api_fixtures()->createUser(['display_name' => 'Direct Login Customer Beta']);
|
|
$subuser = api_fixtures()->createSubuser(['name' => 'Direct Login Driver']);
|
|
api_fixtures()->grantSubuser(
|
|
(int)$subuser['id'],
|
|
(int)$firstCustomer['customer_number'],
|
|
['VEHICLES_LIST']
|
|
);
|
|
$secondGrantId = api_fixtures()->grantSubuser(
|
|
(int)$subuser['id'],
|
|
(int)$secondCustomer['customer_number'],
|
|
['ORDERS_LIST']
|
|
);
|
|
|
|
$ambiguous = api_client()->post(
|
|
'/superuser/subusers/' . $subuser['id'] . '/login-link',
|
|
[],
|
|
$session['headers']
|
|
);
|
|
$direct = api_client()->post(
|
|
'/superuser/subusers/' . $subuser['id'] . '/login-link',
|
|
['grant_id' => $secondGrantId],
|
|
$session['headers']
|
|
);
|
|
|
|
$ambiguous
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Customer number or grant id is required for drivers with multiple customer grants');
|
|
$direct
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($direct->data()['customer_number'] ?? null)->toBe((int)$secondCustomer['customer_number']);
|
|
expect($direct->data()['login_path'] ?? null)->toBeString();
|
|
|
|
$parts = parse_url((string)$direct->data()['login_path']);
|
|
parse_str((string)($parts['query'] ?? ''), $query);
|
|
|
|
expect($parts['path'] ?? null)->toBe('/login/qr');
|
|
expect($query['type'] ?? null)->toBe('subuser');
|
|
expect((int)($query['customer_number'] ?? 0))->toBe((int)$secondCustomer['customer_number']);
|
|
expect($query['token'] ?? null)->toBeString();
|
|
|
|
$resolved = (new \objects\subusers_o())->getSubuserBySessionToken((string)$query['token']);
|
|
expect($resolved)->not->toBeNull();
|
|
expect((int)$resolved->id)->toBe((int)$subuser['id']);
|
|
(new \objects\subusers_o())->invalidateSessionToken((string)$query['token']);
|
|
});
|
|
|
|
it('rejects mismatched customer numbers on user-scoped chauffeur invites', function (): void {
|
|
$session = api_fixtures()->createUserSession(['add_subusers']);
|
|
$targetCustomer = api_fixtures()->createUser(['display_name' => 'Scoped Invite Customer']);
|
|
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Scoped Invite Other']);
|
|
|
|
$response = api_client()->post('/superuser/users/' . $targetCustomer['id'] . '/subusers/invite', [
|
|
'customer_number' => (int)$otherCustomer['customer_number'],
|
|
'name' => 'Mismatched Driver',
|
|
'phone_country_code' => 45,
|
|
'phone' => 71999999,
|
|
], $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Customer number does not match selected user');
|
|
});
|
|
|
|
it('resends pending user-scoped chauffeur invites and blocks accepted accounts', function (): void {
|
|
$session = api_fixtures()->createUserSession(['edit_subusers']);
|
|
$targetCustomer = api_fixtures()->createUser(['display_name' => 'Scoped Resend Customer']);
|
|
$pendingSubuser = api_fixtures()->createSubuser([
|
|
'name' => 'Pending Resend Driver',
|
|
'password_plaintext' => null,
|
|
]);
|
|
$acceptedSubuser = api_fixtures()->createSubuser(['name' => 'Accepted Resend Driver']);
|
|
api_fixtures()->grantSubuser(
|
|
(int)$pendingSubuser['id'],
|
|
(int)$targetCustomer['customer_number'],
|
|
['VEHICLES_LIST']
|
|
);
|
|
api_fixtures()->grantSubuser(
|
|
(int)$acceptedSubuser['id'],
|
|
(int)$targetCustomer['customer_number'],
|
|
['VEHICLES_LIST']
|
|
);
|
|
|
|
$pending = api_client()->post(
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers/' . $pendingSubuser['id'] . '/invite/resend',
|
|
[],
|
|
$session['headers']
|
|
);
|
|
$accepted = api_client()->post(
|
|
'/superuser/users/' . $targetCustomer['id'] . '/subusers/' . $acceptedSubuser['id'] . '/invite/resend',
|
|
[],
|
|
$session['headers']
|
|
);
|
|
|
|
$pending
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
$accepted
|
|
->assertStatus(409)
|
|
->assertEnvelope()
|
|
->assertSuccess(false);
|
|
|
|
$token = $pending->data()['invite']['setup_token'] ?? null;
|
|
expect($token)->toBeString();
|
|
(new \objects\subusers_o())->invalidateSetupToken((string)$token);
|
|
});
|