@fleetbase/fleetops-engine 0.6.68 → 0.6.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE.md +11 -9
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/translations/en-us.yaml +37 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
if (!function_exists('Fleetbase\\Models\\env')) {
|
|
4
|
+
eval('namespace Fleetbase\\Models; function env($key = null, $default = null) { return $default; }');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (!function_exists('Fleetbase\\Notifications\\env')) {
|
|
8
|
+
eval('namespace Fleetbase\\Notifications; function env($key = null, $default = null) { return $default; }');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!function_exists('Fleetbase\\Support\\env')) {
|
|
12
|
+
eval('namespace Fleetbase\\Support; function env($key = null, $default = null) { return $default; }');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!function_exists('Fleetbase\\FleetOps\\Http\\Controllers\\Internal\\v1\\env')) {
|
|
16
|
+
eval('namespace Fleetbase\\FleetOps\\Http\\Controllers\\Internal\\v1; function env($key = null, $default = null) { return $key === \'DEBUG\' ? true : $default; }');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\DriverController;
|
|
20
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\FleetOpsLookupController;
|
|
21
|
+
use Fleetbase\Services\SmsService;
|
|
22
|
+
use Illuminate\Database\ConnectionResolver;
|
|
23
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
24
|
+
use Illuminate\Database\SQLiteConnection;
|
|
25
|
+
use Illuminate\Http\Request;
|
|
26
|
+
use Illuminate\Support\MessageBag;
|
|
27
|
+
use Illuminate\Support\Str;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Covers the internal DriverController login management endpoints against
|
|
31
|
+
* SQLite with real spatie roles: sending and resetting credentials,
|
|
32
|
+
* deactivating (and signing out) and reactivating a driver login, refusing
|
|
33
|
+
* all of them for a driver linked to a team member's account, the proxy
|
|
34
|
+
* field sync on update, and FleetOpsLookupController::profileIdentity.
|
|
35
|
+
*/
|
|
36
|
+
if (!function_exists('Fleetbase\Observers\event')) {
|
|
37
|
+
eval('namespace Fleetbase\Observers; function event($event = null, $payload = []) { return []; }');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!function_exists('Fleetbase\FleetOps\Observers\event')) {
|
|
41
|
+
eval('namespace Fleetbase\FleetOps\Observers; function event($event = null, $payload = []) { return []; }');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!function_exists('Fleetbase\Support\session')) {
|
|
45
|
+
eval('namespace Fleetbase\Support; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } }; } return \session($key, $default); }');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!function_exists('Fleetbase\Models\session')) {
|
|
49
|
+
eval('namespace Fleetbase\Models; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } public function missing($k) { return \session($k) === null; } }; } return \session($key, $default); }');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!function_exists('Fleetbase\Support\auth')) {
|
|
53
|
+
eval('namespace Fleetbase\Support; function auth() { return new class { public function user() { return null; } public function id() { return null; } }; }');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!function_exists('Fleetbase\FleetOps\Http\Controllers\Internal\v1\env')) {
|
|
57
|
+
eval('namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1; function env($key, $default = null) { return $default; }');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!function_exists('__')) {
|
|
61
|
+
function __($key = null, $replace = [], $locale = null)
|
|
62
|
+
{
|
|
63
|
+
return $key;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!class_exists('Illuminate\Validation\Rule')) {
|
|
68
|
+
eval('namespace Illuminate\Validation; class Rule { public function __construct(private string $rule = "") {} public static function requiredIf($c): string { return (is_callable($c) ? $c() : $c) ? "required" : "nullable"; } public static function in(array $v): self { return new self("in:" . implode(",", $v)); } public static function exists($t, $c = null): self { return new self("exists:" . $t . ($c ? "," . $c : "")); } public static function unique($t, $c = null): self { return new self("unique:" . $t . ($c ? "," . $c : "")); } public static function when($c, array $r): array { return (is_callable($c) ? $c() : $c) ? $r : []; } public function where($cb): self { return $this; } public function whereNull($col): self { return $this; } public function ignore($v, $c = null): self { return $this; } public function __toString(): string { return $this->rule; } }');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!Str::hasMacro('humanize')) {
|
|
72
|
+
Str::macro('humanize', fn ($value) => ucfirst(str_replace(['_', '-'], ' ', Str::snake((string) $value))));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!Request::hasMacro('getController')) {
|
|
76
|
+
Request::macro('getController', fn () => new DriverController());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!Request::hasMacro('or')) {
|
|
80
|
+
Request::macro('or', function (array $params = [], $default = null) {
|
|
81
|
+
foreach ($params as $param) {
|
|
82
|
+
if ($this->has($param)) {
|
|
83
|
+
return $this->input($param);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return $default;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fleetopsDriverLoginContainer(): void
|
|
92
|
+
{
|
|
93
|
+
$current = Illuminate\Container\Container::getInstance();
|
|
94
|
+
if (method_exists($current, 'hasDebugModeEnabled')) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Core record mutation surfaces exceptions through app()->hasDebugModeEnabled()
|
|
99
|
+
// and app()->environment(), which the harness container lacks
|
|
100
|
+
$replacement = new class extends Illuminate\Container\Container {
|
|
101
|
+
public function environment(...$environments)
|
|
102
|
+
{
|
|
103
|
+
if (empty($environments)) {
|
|
104
|
+
return 'testing';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$checks = is_array($environments[0]) ? $environments[0] : $environments;
|
|
108
|
+
|
|
109
|
+
return in_array('testing', $checks, true);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public function hasDebugModeEnabled()
|
|
113
|
+
{
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
foreach (['bindings', 'instances', 'aliases', 'abstractAliases', 'resolved', 'extenders', 'tags', 'contextual', 'scopedInstances', 'reboundCallbacks', 'globalBeforeResolvingCallbacks', 'globalResolvingCallbacks', 'globalAfterResolvingCallbacks', 'beforeResolvingCallbacks', 'resolvingCallbacks', 'afterResolvingCallbacks'] as $property) {
|
|
119
|
+
if (!property_exists(Illuminate\Container\Container::class, $property)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
$reflection = new ReflectionProperty(Illuminate\Container\Container::class, $property);
|
|
123
|
+
$reflection->setAccessible(true);
|
|
124
|
+
if ($reflection->isInitialized($current)) {
|
|
125
|
+
$reflection->setValue($replacement, $reflection->getValue($current));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Illuminate\Container\Container::setInstance($replacement);
|
|
130
|
+
Illuminate\Support\Facades\Facade::setFacadeApplication($replacement);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fleetopsDriverLoginBoot(array $validatorErrors): SQLiteConnection
|
|
134
|
+
{
|
|
135
|
+
fleetopsDriverLoginContainer();
|
|
136
|
+
$pdo = new PDO('sqlite::memory:');
|
|
137
|
+
$pdo->sqliteCreateFunction('ST_PointFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
|
|
138
|
+
$pdo->sqliteCreateFunction('ST_GeomFromText', fn ($wkt, $srid = 0, $axisOrder = null) => $wkt);
|
|
139
|
+
$connection = new SQLiteConnection($pdo);
|
|
140
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
141
|
+
$resolver->setDefaultConnection('mysql');
|
|
142
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
143
|
+
if (!EloquentModel::getEventDispatcher()) {
|
|
144
|
+
EloquentModel::setEventDispatcher(new Illuminate\Events\Dispatcher());
|
|
145
|
+
}
|
|
146
|
+
app()->instance('db', new class($connection) {
|
|
147
|
+
public function __construct(public SQLiteConnection $c)
|
|
148
|
+
{
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public function connection($name = null): SQLiteConnection
|
|
152
|
+
{
|
|
153
|
+
return $this->c;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public function __call($method, $arguments)
|
|
157
|
+
{
|
|
158
|
+
return $this->c->{$method}(...$arguments);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
app()->instance('db.schema', $connection->getSchemaBuilder());
|
|
162
|
+
app()->instance('responsecache', new class {
|
|
163
|
+
public function __call($method, $arguments)
|
|
164
|
+
{
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
$GLOBALS['fleetopsDriverLoginErrors'] = $validatorErrors;
|
|
169
|
+
app()->instance('validator', new class {
|
|
170
|
+
public function make($data = [], $rules = [], $messages = [], $attributes = [])
|
|
171
|
+
{
|
|
172
|
+
return new class implements Illuminate\Contracts\Validation\Validator {
|
|
173
|
+
public function fails()
|
|
174
|
+
{
|
|
175
|
+
return !empty($GLOBALS['fleetopsDriverLoginErrors']);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public function errors()
|
|
179
|
+
{
|
|
180
|
+
return new MessageBag($GLOBALS['fleetopsDriverLoginErrors']);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public function validated()
|
|
184
|
+
{
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public function validate()
|
|
189
|
+
{
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public function failed()
|
|
194
|
+
{
|
|
195
|
+
return array_keys($GLOBALS['fleetopsDriverLoginErrors']);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public function sometimes($attribute, $rules, callable $callback)
|
|
199
|
+
{
|
|
200
|
+
return $this;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public function after($callback)
|
|
204
|
+
{
|
|
205
|
+
return $this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public function getMessageBag()
|
|
209
|
+
{
|
|
210
|
+
return new MessageBag($GLOBALS['fleetopsDriverLoginErrors']);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
Illuminate\Support\Facades\DB::clearResolvedInstance('db');
|
|
216
|
+
app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
|
|
217
|
+
public function info($hashedValue): array
|
|
218
|
+
{
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public function make($value, array $options = []): string
|
|
223
|
+
{
|
|
224
|
+
return 'hashed:' . $value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public function check($value, $hashedValue, array $options = []): bool
|
|
228
|
+
{
|
|
229
|
+
return 'hashed:' . $value === $hashedValue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public function needsRehash($hashedValue, array $options = []): bool
|
|
233
|
+
{
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
|
|
238
|
+
|
|
239
|
+
$schema = $connection->getSchemaBuilder();
|
|
240
|
+
$tables = [
|
|
241
|
+
'drivers' => ['uuid', 'public_id', 'internal_id', 'company_uuid', 'user_uuid', 'vehicle_uuid', 'vendor_uuid', 'current_job_uuid', 'auth_token', 'signup_token_used', 'avatar_url', 'drivers_license_number', 'license_expiry', 'location', 'heading', 'bearing', 'altitude', 'speed', 'currency', 'current_status', 'meta', 'location_updated_at', 'slug', 'status', 'country', 'city', 'online', '_key'],
|
|
242
|
+
'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'email', 'phone', 'password', 'status', 'type', 'username', 'avatar_uuid', 'slug', 'timezone', 'country', 'ip_address', 'meta', '_key'],
|
|
243
|
+
'companies' => ['uuid', 'public_id', 'name', 'owner_uuid', 'timezone', 'options', 'status', '_key'],
|
|
244
|
+
'company_users' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'status', '_key'],
|
|
245
|
+
'vehicles' => ['uuid', 'public_id', 'company_uuid', 'driver_uuid'],
|
|
246
|
+
'custom_fields' => ['uuid', 'public_id', 'company_uuid', 'subject_uuid', 'subject_type', 'name', 'label'],
|
|
247
|
+
'custom_field_values' => ['uuid', 'public_id', 'company_uuid', 'custom_field_uuid', 'subject_uuid', 'subject_type', 'value', 'value_type', '_key'],
|
|
248
|
+
'settings' => ['key', 'value'],
|
|
249
|
+
'permissions' => ['name', 'guard_name', 'service', 'description'],
|
|
250
|
+
'roles' => ['uuid', 'public_id', 'name', 'guard_name', 'company_uuid', 'service', 'description', '_key'],
|
|
251
|
+
'model_has_roles' => ['role_id', 'model_type', 'model_uuid'],
|
|
252
|
+
'model_has_permissions' => ['permission_id', 'model_type', 'model_uuid'],
|
|
253
|
+
'role_has_permissions' => ['permission_id', 'role_id'],
|
|
254
|
+
'policies' => ['name', 'guard_name', 'company_uuid'],
|
|
255
|
+
'directives' => ['uuid', 'public_id', 'company_uuid', 'permission_uuid', 'subject_type', 'subject_uuid', 'key', 'rules'],
|
|
256
|
+
'files' => ['uuid', 'public_id', 'company_uuid', 'uploader_uuid', 'subject_uuid', 'subject_type', 'name', 'original_filename', 'extension', 'content_type', 'path', 'bucket', 'disk', 'size', 'type', 'meta', '_key'],
|
|
257
|
+
'invites' => ['uuid', 'public_id', 'company_uuid', 'created_by_uuid', 'subject_uuid', 'subject_type', 'code', 'uri', 'protocol', 'recipients', 'reason', 'expires_at', '_key'],
|
|
258
|
+
'notifications' => ['type', 'notifiable_type', 'notifiable_id', 'data', 'read_at'],
|
|
259
|
+
'contacts' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'name', 'email', 'phone', 'type'],
|
|
260
|
+
'personal_access_tokens' => ['tokenable_type', 'tokenable_id', 'name', 'token', 'abilities', 'last_used_at', 'expires_at'],
|
|
261
|
+
];
|
|
262
|
+
foreach ($tables as $table => $columns) {
|
|
263
|
+
$schema->create($table, function ($blueprint) use ($columns) {
|
|
264
|
+
$blueprint->increments('id');
|
|
265
|
+
foreach ($columns as $column) {
|
|
266
|
+
$blueprint->string($column)->nullable();
|
|
267
|
+
}
|
|
268
|
+
$blueprint->timestamps();
|
|
269
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
app()->instance('cache', new class {
|
|
274
|
+
public function tags($tags = null)
|
|
275
|
+
{
|
|
276
|
+
return $this;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public function flush()
|
|
280
|
+
{
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
public function remember($key, $ttl, $callback)
|
|
285
|
+
{
|
|
286
|
+
return $callback();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public function store($name = null)
|
|
290
|
+
{
|
|
291
|
+
return $this;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
public function __call($method, $arguments)
|
|
295
|
+
{
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
Illuminate\Support\Facades\Cache::clearResolvedInstance('cache');
|
|
300
|
+
app()->instance(Illuminate\Contracts\Notifications\Dispatcher::class, new class {
|
|
301
|
+
public array $sent = [];
|
|
302
|
+
|
|
303
|
+
public function send($notifiables, $notification)
|
|
304
|
+
{
|
|
305
|
+
$this->sent[] = $notification;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public function sendNow($notifiables, $notification, ?array $channels = null)
|
|
309
|
+
{
|
|
310
|
+
$this->sent[] = $notification;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public function __call($method, $arguments)
|
|
314
|
+
{
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
config()->set('cache.default', 'array');
|
|
319
|
+
config()->set('cache.stores.array', ['driver' => 'array']);
|
|
320
|
+
config()->set('permission.cache.expiration_time', 60);
|
|
321
|
+
config()->set('permission.cache.key', 'spatie.permission.cache');
|
|
322
|
+
config()->set('permission.cache.store', 'default');
|
|
323
|
+
config()->set('permission.models.permission', Fleetbase\Models\Permission::class);
|
|
324
|
+
config()->set('permission.models.role', Fleetbase\Models\Role::class);
|
|
325
|
+
config()->set('permission.table_names', ['roles' => 'roles', 'permissions' => 'permissions', 'model_has_permissions' => 'model_has_permissions', 'model_has_roles' => 'model_has_roles', 'role_has_permissions' => 'role_has_permissions']);
|
|
326
|
+
config()->set('permission.column_names', ['role_pivot_key' => null, 'permission_pivot_key' => null, 'model_morph_key' => 'model_uuid', 'team_foreign_key' => 'team_id']);
|
|
327
|
+
config()->set('permission.teams', false);
|
|
328
|
+
config()->set('permission.events_enabled', false);
|
|
329
|
+
$cacheManager = new Illuminate\Cache\CacheManager(app());
|
|
330
|
+
app()->instance(Illuminate\Cache\CacheManager::class, $cacheManager);
|
|
331
|
+
app()->instance(Spatie\Permission\PermissionRegistrar::class, new Spatie\Permission\PermissionRegistrar($cacheManager));
|
|
332
|
+
config()->set('activitylog.enabled', false);
|
|
333
|
+
config()->set('activitylog.default_auth_driver', 'web');
|
|
334
|
+
config()->set('auth.defaults.guard', 'web');
|
|
335
|
+
config()->set('permission.models.role', Fleetbase\Models\Role::class);
|
|
336
|
+
app()->bind(Illuminate\Contracts\Config\Repository::class, fn () => config());
|
|
337
|
+
|
|
338
|
+
session(['company' => 'company-1']);
|
|
339
|
+
$connection->table('companies')->insert(['uuid' => 'company-1', 'public_id' => 'company_adopt1', 'name' => 'Acme']);
|
|
340
|
+
$connection->table('roles')->insert([
|
|
341
|
+
['uuid' => 'role-1', 'name' => 'Driver', 'guard_name' => 'web', 'company_uuid' => 'company-1'],
|
|
342
|
+
['uuid' => 'role-2', 'name' => 'Driver', 'guard_name' => 'sanctum', 'company_uuid' => 'company-1'],
|
|
343
|
+
['uuid' => 'role-3', 'name' => 'Administrator', 'guard_name' => 'web', 'company_uuid' => 'company-1'],
|
|
344
|
+
['uuid' => 'role-4', 'name' => 'Administrator', 'guard_name' => 'sanctum', 'company_uuid' => 'company-1'],
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
return $connection;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
class FleetOpsDriverLoginSmsFake extends SmsService
|
|
351
|
+
{
|
|
352
|
+
public array $sent = [];
|
|
353
|
+
|
|
354
|
+
public function __construct()
|
|
355
|
+
{
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
public function send(string $to, string $text, array $options = [], ?string $provider = null): array
|
|
359
|
+
{
|
|
360
|
+
$this->sent[] = [$to, $text];
|
|
361
|
+
|
|
362
|
+
return ['success' => true];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function fleetopsDriverLoginFixture(): SQLiteConnection
|
|
367
|
+
{
|
|
368
|
+
$connection = fleetopsDriverLoginBoot([]);
|
|
369
|
+
|
|
370
|
+
Illuminate\Support\Facades\Mail::swap(new class {
|
|
371
|
+
public array $sent = [];
|
|
372
|
+
|
|
373
|
+
public function to($users)
|
|
374
|
+
{
|
|
375
|
+
return $this;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
public function send($mailable)
|
|
379
|
+
{
|
|
380
|
+
$this->sent[] = $mailable;
|
|
381
|
+
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
app()->instance(SmsService::class, new FleetOpsDriverLoginSmsFake());
|
|
386
|
+
|
|
387
|
+
$users = [
|
|
388
|
+
['uuid' => '11111111-1111-4111-8111-111111111111', 'public_id' => 'user_managed', 'company_uuid' => 'company-1', 'name' => 'Managed Driver', 'email' => 'managed@example.com', 'phone' => '+6590000001', 'password' => 'hashed:old', 'status' => 'pending', 'type' => 'driver'],
|
|
389
|
+
['uuid' => '22222222-2222-4222-8222-222222222222', 'public_id' => 'user_staff', 'company_uuid' => 'company-1', 'name' => 'Staff Driver', 'email' => 'staff@example.com', 'phone' => '+6590000002', 'password' => 'hashed:staff', 'status' => 'active', 'type' => 'user'],
|
|
390
|
+
['uuid' => '33333333-3333-4333-8333-333333333333', 'public_id' => 'user_nocontact', 'company_uuid' => 'company-1', 'name' => 'Unreachable', 'status' => 'active', 'type' => 'driver'],
|
|
391
|
+
];
|
|
392
|
+
foreach ($users as $user) {
|
|
393
|
+
$connection->table('users')->insert($user);
|
|
394
|
+
}
|
|
395
|
+
$connection->table('company_users')->insert(['uuid' => 'cu-managed', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111', 'status' => 'pending']);
|
|
396
|
+
$drivers = [
|
|
397
|
+
['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', 'public_id' => 'driver_managed', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111'],
|
|
398
|
+
['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2', 'public_id' => 'driver_staff', 'company_uuid' => 'company-1', 'user_uuid' => '22222222-2222-4222-8222-222222222222'],
|
|
399
|
+
['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3', 'public_id' => 'driver_nocontact', 'company_uuid' => 'company-1', 'user_uuid' => '33333333-3333-4333-8333-333333333333'],
|
|
400
|
+
];
|
|
401
|
+
foreach ($drivers as $driver) {
|
|
402
|
+
$connection->table('drivers')->insert($driver);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return $connection;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function fleetopsDriverLoginUser(SQLiteConnection $connection, string $uuid = '11111111-1111-4111-8111-111111111111'): object
|
|
409
|
+
{
|
|
410
|
+
return $connection->table('users')->where('uuid', $uuid)->first();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
test('send credentials sets a new password activates the login and emails it', function () {
|
|
414
|
+
$connection = fleetopsDriverLoginFixture();
|
|
415
|
+
|
|
416
|
+
$response = (new DriverController())->sendCredentials('driver_managed');
|
|
417
|
+
$data = $response->getData(true);
|
|
418
|
+
$user = fleetopsDriverLoginUser($connection);
|
|
419
|
+
|
|
420
|
+
expect($data['status'])->toBe('ok')
|
|
421
|
+
->and($data['sent_via'])->toBe('email')
|
|
422
|
+
->and($data['driver'])->toMatchArray([
|
|
423
|
+
'id' => 'driver_managed',
|
|
424
|
+
'uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1',
|
|
425
|
+
'user_uuid' => '11111111-1111-4111-8111-111111111111',
|
|
426
|
+
'is_staff_linked' => false,
|
|
427
|
+
'login_status' => 'active',
|
|
428
|
+
])
|
|
429
|
+
->and($data['driver']['user'])->toMatchArray([
|
|
430
|
+
'id' => 'user_managed',
|
|
431
|
+
'email' => 'managed@example.com',
|
|
432
|
+
'phone' => '+6590000001',
|
|
433
|
+
'status' => 'active',
|
|
434
|
+
])
|
|
435
|
+
->and($user->status)->toBe('active')
|
|
436
|
+
->and($user->password)->not->toBe('hashed:old')
|
|
437
|
+
->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent[0])->toBeInstanceOf(Fleetbase\FleetOps\Mail\DriverCredentialsMail::class);
|
|
438
|
+
|
|
439
|
+
// A driver with neither an email nor a phone can't be sent credentials
|
|
440
|
+
$unreachable = (new DriverController())->sendCredentials('driver_nocontact');
|
|
441
|
+
expect($unreachable->getData(true))->toBe(['error' => 'This profile has no email or phone to send credentials to.']);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('login management endpoints report missing drivers accounts and staff-linked logins', function () {
|
|
445
|
+
$connection = fleetopsDriverLoginFixture();
|
|
446
|
+
// The driver scope keeps drivers whose account was deleted (the user
|
|
447
|
+
// relation includes trashed accounts), but such a driver has no login
|
|
448
|
+
$connection->table('users')->insert(['uuid' => '44444444-4444-4444-8444-444444444444', 'company_uuid' => 'company-1', 'type' => 'driver', 'deleted_at' => '2026-01-01 00:00:00']);
|
|
449
|
+
$connection->table('drivers')->insert(['uuid' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa4', 'public_id' => 'driver_orphan', 'company_uuid' => 'company-1', 'user_uuid' => '44444444-4444-4444-8444-444444444444']);
|
|
450
|
+
$controller = new DriverController();
|
|
451
|
+
$reset = fn (string $id) => $controller->resetCredentials(Request::create('/x', 'POST', ['password' => 'long-enough', 'password_confirmation' => 'long-enough']), $id);
|
|
452
|
+
|
|
453
|
+
foreach ([
|
|
454
|
+
fn (string $id) => $controller->sendCredentials($id),
|
|
455
|
+
$reset,
|
|
456
|
+
fn (string $id) => $controller->deactivateLogin($id),
|
|
457
|
+
fn (string $id) => $controller->reactivateLogin($id),
|
|
458
|
+
] as $endpoint) {
|
|
459
|
+
$missing = $endpoint('driver_unknown');
|
|
460
|
+
$orphan = $endpoint('driver_orphan');
|
|
461
|
+
$staff = $endpoint('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2');
|
|
462
|
+
|
|
463
|
+
expect($missing->getStatusCode())->toBe(404)
|
|
464
|
+
->and($missing->getData(true))->toBe(['error' => 'Driver not found.'])
|
|
465
|
+
->and($orphan->getData(true))->toBe(['error' => 'This driver has no login account.'])
|
|
466
|
+
->and($staff->getStatusCode())->toBe(422)
|
|
467
|
+
->and($staff->getData(true))->toBe(['error' => 'This driver signs in with a team member account. Manage this login in IAM.']);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// The team member's account is never touched
|
|
471
|
+
$staffUser = fleetopsDriverLoginUser($connection, '22222222-2222-4222-8222-222222222222');
|
|
472
|
+
expect($staffUser->password)->toBe('hashed:staff')
|
|
473
|
+
->and($staffUser->status)->toBe('active')
|
|
474
|
+
->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent)->toBe([]);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test('reset credentials validates the password and optionally sends it', function () {
|
|
478
|
+
$connection = fleetopsDriverLoginFixture();
|
|
479
|
+
$controller = new DriverController();
|
|
480
|
+
$request = fn (array $input) => Request::create('/x', 'POST', $input);
|
|
481
|
+
|
|
482
|
+
expect($controller->resetCredentials($request(['password' => 'short', 'password_confirmation' => 'short']), 'driver_managed')->getData(true))
|
|
483
|
+
->toBe(['error' => 'Password must be at least 8 characters.'])
|
|
484
|
+
->and($controller->resetCredentials($request([]), 'driver_managed')->getData(true))
|
|
485
|
+
->toBe(['error' => 'Password must be at least 8 characters.'])
|
|
486
|
+
->and($controller->resetCredentials($request(['password' => 'long-enough', 'password_confirmation' => 'different']), 'driver_managed')->getData(true))
|
|
487
|
+
->toBe(['error' => 'Passwords do not match.']);
|
|
488
|
+
|
|
489
|
+
$quiet = $controller->resetCredentials($request(['password' => 'quiet-secret', 'password_confirmation' => 'quiet-secret']), 'driver_managed');
|
|
490
|
+
expect($quiet->getData(true)['status'])->toBe('ok')
|
|
491
|
+
->and(fleetopsDriverLoginUser($connection)->password)->toBe('hashed:quiet-secret')
|
|
492
|
+
->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent)->toBe([]);
|
|
493
|
+
|
|
494
|
+
$sent = $controller->resetCredentials($request(['password' => 'mailed-secret', 'password_confirmation' => 'mailed-secret', 'send_credentials' => true]), 'driver_managed');
|
|
495
|
+
expect($sent->getData(true)['driver']['user_uuid'])->toBe('11111111-1111-4111-8111-111111111111')
|
|
496
|
+
->and(fleetopsDriverLoginUser($connection)->password)->toBe('hashed:mailed-secret')
|
|
497
|
+
->and(Illuminate\Support\Facades\Mail::getFacadeRoot()->sent)->toHaveCount(1);
|
|
498
|
+
|
|
499
|
+
// The password is still changed when there's nowhere to send it
|
|
500
|
+
$unsendable = $controller->resetCredentials($request(['password' => 'kept-secret', 'password_confirmation' => 'kept-secret', 'send_credentials' => '1']), 'driver_nocontact');
|
|
501
|
+
expect($unsendable->getData(true))->toBe(['error' => 'This profile has no email or phone to send credentials to.'])
|
|
502
|
+
->and(fleetopsDriverLoginUser($connection, '33333333-3333-4333-8333-333333333333')->password)->toBe('hashed:kept-secret');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('deactivating a login signs the driver out and reactivating restores it', function () {
|
|
506
|
+
$connection = fleetopsDriverLoginFixture();
|
|
507
|
+
$connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111111')->update(['status' => 'active']);
|
|
508
|
+
$connection->table('personal_access_tokens')->insert([
|
|
509
|
+
'tokenable_type' => Fleetbase\Models\User::class,
|
|
510
|
+
'tokenable_id' => '11111111-1111-4111-8111-111111111111',
|
|
511
|
+
'name' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1',
|
|
512
|
+
'token' => 'token-hash',
|
|
513
|
+
]);
|
|
514
|
+
$connection->table('personal_access_tokens')->insert([
|
|
515
|
+
'tokenable_type' => Fleetbase\Models\User::class,
|
|
516
|
+
'tokenable_id' => '22222222-2222-4222-8222-222222222222',
|
|
517
|
+
'name' => 'staff-session',
|
|
518
|
+
'token' => 'staff-token-hash',
|
|
519
|
+
]);
|
|
520
|
+
$controller = new DriverController();
|
|
521
|
+
|
|
522
|
+
$deactivated = $controller->deactivateLogin('driver_managed')->getData(true);
|
|
523
|
+
|
|
524
|
+
expect($deactivated['status'])->toBe('ok')
|
|
525
|
+
->and($deactivated['driver']['login_status'])->toBe('inactive')
|
|
526
|
+
->and(fleetopsDriverLoginUser($connection)->status)->toBe('inactive')
|
|
527
|
+
->and($connection->table('company_users')->where('uuid', 'cu-managed')->value('status'))->toBe('inactive')
|
|
528
|
+
// Only the driver's own tokens are revoked
|
|
529
|
+
->and($connection->table('personal_access_tokens')->pluck('name')->all())->toBe(['staff-session']);
|
|
530
|
+
|
|
531
|
+
$reactivated = $controller->reactivateLogin('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1')->getData(true);
|
|
532
|
+
|
|
533
|
+
expect($reactivated['driver']['login_status'])->toBe('active')
|
|
534
|
+
->and($reactivated['driver']['user']['session_status'])->toBe('active')
|
|
535
|
+
->and(fleetopsDriverLoginUser($connection)->status)->toBe('active');
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
function fleetopsDriverLoginUpdateRequest(string $id, array $driver): Request
|
|
539
|
+
{
|
|
540
|
+
$request = Request::create('/int/v1/drivers/' . $id, 'PUT', ['driver' => $driver]);
|
|
541
|
+
$store = app('session.store');
|
|
542
|
+
$store->put('company', 'company-1');
|
|
543
|
+
$request->setLaravelSession($store);
|
|
544
|
+
$request->setRouteResolver(fn () => new class($id) {
|
|
545
|
+
public function __construct(private string $id)
|
|
546
|
+
{
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
public function getAction($key = null)
|
|
550
|
+
{
|
|
551
|
+
return DriverController::class . '@updateRecord';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
public function getActionMethod()
|
|
555
|
+
{
|
|
556
|
+
return 'updateRecord';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
public function uri()
|
|
560
|
+
{
|
|
561
|
+
return 'int/v1/drivers/{id}';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public function getName()
|
|
565
|
+
{
|
|
566
|
+
return 'int.v1.drivers.update';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
public function parameters()
|
|
570
|
+
{
|
|
571
|
+
return ['id' => $this->id];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
public function parameter($key, $default = null)
|
|
575
|
+
{
|
|
576
|
+
return $key === 'id' ? $this->id : $default;
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
app()->instance('request', $request);
|
|
580
|
+
|
|
581
|
+
return $request;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
test('update record syncs proxy fields to a managed account and only the name to a team member', function () {
|
|
585
|
+
$connection = fleetopsDriverLoginFixture();
|
|
586
|
+
$controller = new DriverController();
|
|
587
|
+
|
|
588
|
+
$managed = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', [
|
|
589
|
+
'name' => 'Renamed Driver',
|
|
590
|
+
'email' => 'Renamed@Example.com',
|
|
591
|
+
'phone' => null,
|
|
592
|
+
'photo_uuid' => 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
|
|
593
|
+
'user_uuid' => '22222222-2222-4222-8222-222222222222',
|
|
594
|
+
]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
|
|
595
|
+
|
|
596
|
+
$user = fleetopsDriverLoginUser($connection);
|
|
597
|
+
|
|
598
|
+
expect($managed)->toBeArray()
|
|
599
|
+
->and($managed['driver']->resource->user_uuid)->toBe('11111111-1111-4111-8111-111111111111')
|
|
600
|
+
->and($user->name)->toBe('Renamed Driver')
|
|
601
|
+
->and($user->email)->toBe('renamed@example.com')
|
|
602
|
+
->and($user->phone)->toBeNull()
|
|
603
|
+
->and($user->avatar_uuid)->toBe('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb');
|
|
604
|
+
|
|
605
|
+
$staff = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2', [
|
|
606
|
+
'name' => 'Staff As Driver',
|
|
607
|
+
'email' => 'changed@example.com',
|
|
608
|
+
]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2');
|
|
609
|
+
|
|
610
|
+
$staffUser = fleetopsDriverLoginUser($connection, '22222222-2222-4222-8222-222222222222');
|
|
611
|
+
|
|
612
|
+
expect($staff)->toBeArray()
|
|
613
|
+
->and($staffUser->name)->toBe('Staff As Driver')
|
|
614
|
+
->and($staffUser->email)->toBe('staff@example.com');
|
|
615
|
+
|
|
616
|
+
// Taking another account's email is refused
|
|
617
|
+
$conflict = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', [
|
|
618
|
+
'email' => 'staff@example.com',
|
|
619
|
+
]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
|
|
620
|
+
|
|
621
|
+
expect($conflict->getStatusCode())->toBe(422)
|
|
622
|
+
->and($conflict->getData(true))->toBe(['error' => 'This email is already in use by another account.'])
|
|
623
|
+
->and(fleetopsDriverLoginUser($connection)->email)->toBe('renamed@example.com');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
function fleetopsDriverLoginLookup(array $input): array
|
|
627
|
+
{
|
|
628
|
+
return (new FleetOpsLookupController())->profileIdentity(Request::create('/int/v1/fleet-ops/lookup/profile-identity', 'GET', $input))->getData(true);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
test('profile identity lookup reports the team member to link or the conflict', function () {
|
|
632
|
+
$connection = fleetopsDriverLoginFixture();
|
|
633
|
+
$connection->table('users')->insert(['uuid' => '55555555-5555-4555-8555-555555555555', 'company_uuid' => 'company-1', 'name' => 'Office Staff', 'email' => 'office@example.com', 'phone' => '+6590000005', 'type' => 'admin']);
|
|
634
|
+
$connection->table('users')->insert(['uuid' => '66666666-6666-4666-8666-666666666666', 'company_uuid' => 'company-1', 'name' => 'Customer', 'email' => 'customer@example.com', 'type' => 'customer']);
|
|
635
|
+
$connection->table('contacts')->insert(['uuid' => 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', 'public_id' => 'contact_customer', 'company_uuid' => 'company-1', 'user_uuid' => '66666666-6666-4666-8666-666666666666', 'type' => 'customer']);
|
|
636
|
+
$connection->table('contacts')->insert(['uuid' => 'cccccccc-cccc-4ccc-8ccc-ccccccccccc2', 'public_id' => 'contact_nouser', 'company_uuid' => 'company-1', 'type' => 'contact']);
|
|
637
|
+
|
|
638
|
+
expect((new FleetOpsLookupController())->profileIdentity(Request::create('/x', 'GET', ['type' => 'user']))->getStatusCode())->toBe(422)
|
|
639
|
+
->and(fleetopsDriverLoginLookup(['type' => 'staff']))->toBe(['error' => 'Invalid profile type.'])
|
|
640
|
+
// Nothing entered, nothing found
|
|
641
|
+
->and(fleetopsDriverLoginLookup([]))->toBe(['staff' => null, 'conflict' => null])
|
|
642
|
+
// A new driver with a team member's email links to them
|
|
643
|
+
->and(fleetopsDriverLoginLookup(['email' => 'office@example.com']))->toBe([
|
|
644
|
+
'staff' => ['uuid' => '55555555-5555-4555-8555-555555555555', 'name' => 'Office Staff', 'email' => 'office@example.com', 'phone' => '+6590000005'],
|
|
645
|
+
'conflict' => null,
|
|
646
|
+
])
|
|
647
|
+
// A new driver can't take a customer's email
|
|
648
|
+
->and(fleetopsDriverLoginLookup(['type' => 'driver', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => 'This email is already used by a customer.'])
|
|
649
|
+
// An existing driver's own email isn't taken, another account's is
|
|
650
|
+
->and(fleetopsDriverLoginLookup(['ignore' => 'driver_managed', 'email' => 'managed@example.com']))->toBe(['staff' => null, 'conflict' => null])
|
|
651
|
+
->and(fleetopsDriverLoginLookup(['ignore' => 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', 'phone' => '+6590000005']))->toBe(['staff' => null, 'conflict' => 'This phone number is already in use by another account.'])
|
|
652
|
+
// Customer profiles are looked up among contacts
|
|
653
|
+
->and(fleetopsDriverLoginLookup(['type' => 'customer', 'ignore' => 'contact_customer', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => null])
|
|
654
|
+
// A profile without an account, or an unknown profile id, is treated as a new profile
|
|
655
|
+
->and(fleetopsDriverLoginLookup(['type' => 'contact', 'ignore' => 'contact_nouser', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => 'This email is already used by a customer.'])
|
|
656
|
+
->and(fleetopsDriverLoginLookup(['type' => 'contact', 'ignore' => 'contact_unknown', 'email' => 'customer@example.com']))->toBe(['staff' => null, 'conflict' => 'This email is already used by a customer.']);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
test('update record syncs custom field values and reports failures', function () {
|
|
660
|
+
$connection = fleetopsDriverLoginFixture();
|
|
661
|
+
$connection->table('custom_fields')->insert(['uuid' => 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', 'company_uuid' => 'company-1', 'name' => 'badge', 'label' => 'Badge']);
|
|
662
|
+
$controller = new DriverController();
|
|
663
|
+
|
|
664
|
+
$updated = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', [
|
|
665
|
+
'custom_field_values' => [['custom_field_uuid' => 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', 'value' => 'B-12']],
|
|
666
|
+
]), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
|
|
667
|
+
|
|
668
|
+
expect($updated)->toBeArray()
|
|
669
|
+
->and($connection->table('custom_field_values')->where('subject_uuid', 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1')->value('value'))->toBe('B-12');
|
|
670
|
+
|
|
671
|
+
// An unknown driver surfaces the model's not-found error
|
|
672
|
+
$missing = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa9', ['name' => 'Ghost']), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa9');
|
|
673
|
+
expect($missing->getData(true)['error'])->toContain('not found');
|
|
674
|
+
|
|
675
|
+
// A request validation failure raised while updating the account is returned
|
|
676
|
+
$GLOBALS['fleetopsDriverLoginHashException'] = new Fleetbase\Exceptions\FleetbaseRequestValidationException(['password' => ['The password is too weak.']]);
|
|
677
|
+
app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
|
|
678
|
+
public function info($hashedValue): array
|
|
679
|
+
{
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
public function make($value, array $options = []): string
|
|
684
|
+
{
|
|
685
|
+
throw $GLOBALS['fleetopsDriverLoginHashException'];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
public function check($value, $hashedValue, array $options = []): bool
|
|
689
|
+
{
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
public function needsRehash($hashedValue, array $options = []): bool
|
|
694
|
+
{
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
|
|
699
|
+
|
|
700
|
+
$weak = $controller->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', ['password' => 'weak-password']), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
|
|
701
|
+
expect($weak->getData(true))->toBe(['error' => ['password' => ['The password is too weak.']]]);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('update record returns validation failures', function () {
|
|
705
|
+
fleetopsDriverLoginFixture();
|
|
706
|
+
$GLOBALS['fleetopsDriverLoginErrors'] = ['email' => ['The email must be a valid email address.']];
|
|
707
|
+
|
|
708
|
+
$failure = null;
|
|
709
|
+
try {
|
|
710
|
+
(new DriverController())->updateRecord(fleetopsDriverLoginUpdateRequest('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1', ['email' => 'not-an-email']), 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1');
|
|
711
|
+
} catch (Illuminate\Validation\ValidationException $exception) {
|
|
712
|
+
$failure = $exception;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
expect($failure?->errors())->toBe(['email' => ['The email must be a valid email address.']]);
|
|
716
|
+
});
|