Implement subuser verification and invoice/self-serve API fixes
This commit is contained in:
@@ -93,6 +93,131 @@ it('returns a generic error for invalid subuser credentials', function (): void
|
||||
->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');
|
||||
|
||||
@@ -206,7 +331,7 @@ it('validates setup identifiers before setting the driver password', function ()
|
||||
}
|
||||
});
|
||||
|
||||
it('lists chauffeur grants across customers for superusers', function (): void {
|
||||
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',
|
||||
@@ -253,23 +378,77 @@ it('lists chauffeur grants across customers for superusers', function (): void {
|
||||
&& in_array((int)($item['id'] ?? 0), [(int)$firstSubuser['id'], (int)$secondSubuser['id']], true)
|
||||
));
|
||||
|
||||
expect($rows)->toHaveCount(3);
|
||||
expect($rows)->toHaveCount(2);
|
||||
expect($response->meta()['pagination']['total'] ?? null)->toBe(2);
|
||||
|
||||
$byGrantId = [];
|
||||
$bySubuserId = [];
|
||||
foreach ($rows as $row) {
|
||||
$byGrantId[(int)$row['grant_id']] = $row;
|
||||
$bySubuserId[(int)$row['id']] = $row;
|
||||
}
|
||||
|
||||
expect($byGrantId[$firstGrantId]['id'])->toBe((int)$firstSubuser['id']);
|
||||
expect($byGrantId[$firstGrantId]['customer_number'])->toBe((int)$firstCustomer['customer_number']);
|
||||
expect($byGrantId[$firstGrantId]['assigned_vehicle_id'])->toBe((int)$firstVehicle['id']);
|
||||
expect($byGrantId[$firstGrantId]['assigned_vehicle_reg'])->toBe('AB12345');
|
||||
expect($byGrantId[$firstGrantId]['dognvask_enabled'])->toBeTrue();
|
||||
expect($byGrantId[$sharedGrantId]['id'])->toBe((int)$firstSubuser['id']);
|
||||
expect($byGrantId[$sharedGrantId]['customer_number'])->toBe((int)$secondCustomer['customer_number']);
|
||||
expect($byGrantId[$sharedGrantId]['dognvask_enabled'])->toBeFalse();
|
||||
expect($byGrantId[$secondGrantId]['id'])->toBe((int)$secondSubuser['id']);
|
||||
expect($byGrantId[$secondGrantId]['customer_name'])->toBe('Fleet Customer Beta');
|
||||
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 {
|
||||
@@ -565,6 +744,8 @@ it('lets superusers edit chauffeur account details and set passwords', function
|
||||
'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();
|
||||
@@ -602,11 +783,15 @@ it('lets superusers edit chauffeur account details and set passwords', function
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user