Files
api/services/nginx/app/tests/Api/SubusersApiTest.php
T

590 lines
22 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('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 chauffeur 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']);
$firstGrantId = api_fixtures()->grantSubuser(
(int)$firstSubuser['id'],
(int)$firstCustomer['customer_number'],
['VEHICLES_LIST', 'SUBUSERS_LIST']
);
$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);
$bySubuserId = [];
foreach ($rows as $row) {
$bySubuserId[(int)$row['id']] = $row;
}
expect($bySubuserId[(int)$firstSubuser['id']]['grant_count'])->toBe(2);
expect(array_column($bySubuserId[(int)$firstSubuser['id']]['grants'], 'grant_id'))
->toContain($firstGrantId)
->toContain($sharedGrantId);
expect($bySubuserId[(int)$firstSubuser['id']]['customer_numbers'])
->toContain((int)$firstCustomer['customer_number'])
->toContain((int)$secondCustomer['customer_number']);
expect($bySubuserId[(int)$secondSubuser['id']]['grant_count'])->toBe(1);
expect($bySubuserId[(int)$secondSubuser['id']]['grants'][0]['grant_id'])->toBe($secondGrantId);
expect($bySubuserId[(int)$secondSubuser['id']]['grants'][0]['customer_name'])->toBe('Fleet Customer Beta');
});
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']);
$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'],
],
$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();
$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']);
});
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);
});