Skip to content

Commit 92bce94

Browse files
authored
Gracefully handle exceptions while resolving user ids (#216)
1 parent 41d99c1 commit 92bce94

File tree

5 files changed

+209
-12
lines changed

5 files changed

+209
-12
lines changed

src/NightwatchServiceProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ private function executionState(): RequestState|CommandState
469469
currentExecutionStageStartedAtMicrotime: $this->timestamp,
470470
deploy: $this->nightwatchConfig['deployment'] ?? '',
471471
server: $this->nightwatchConfig['server'] ?? '',
472-
user: new UserProvider($auth, fn () => $this->core->userDetailsResolver),
472+
user: new UserProvider($auth, fn () => $this->core->userDetailsResolver, fn () => $this->core->report(...)),
473473
);
474474
} else {
475475
return new CommandState(

src/UserProvider.php

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Auth\AuthManager;
66
use Illuminate\Contracts\Auth\Authenticatable;
77
use Laravel\Nightwatch\Types\Str;
8+
use Throwable;
89

910
use function call_user_func;
1011

@@ -20,27 +21,48 @@ final class UserProvider
2021
*/
2122
public $userDetailsResolverResolver;
2223

24+
/**
25+
* @var (callable(): (callable(Throwable, bool): void))
26+
*/
27+
public $reportResolver;
28+
29+
private bool $alreadyReportedResolvingUserIdException = false;
30+
2331
public function __construct(
2432
private AuthManager $auth,
2533
callable $userDetailsResolverResolver,
34+
callable $reportResolver,
2635
) {
2736
$this->userDetailsResolverResolver = $userDetailsResolverResolver;
37+
$this->reportResolver = $reportResolver;
2838
}
2939

3040
/**
3141
* @return string|LazyValue<string>
3242
*/
3343
public function id(): LazyValue|string
3444
{
35-
if ($this->auth->hasUser()) {
36-
return Str::tinyText((string) $this->auth->id());
45+
try {
46+
if ($this->auth->hasUser()) {
47+
return Str::tinyText((string) $this->auth->id());
48+
}
49+
} catch (Throwable $e) {
50+
$this->reportResolvingUserIdException($e);
51+
52+
return '';
3753
}
3854

3955
return new LazyValue(function () {
40-
if ($this->auth->hasUser()) {
41-
return Str::tinyText((string) $this->auth->id());
42-
} else {
43-
return Str::tinyText((string) $this->rememberedUser?->getAuthIdentifier()); // @phpstan-ignore cast.string
56+
try {
57+
if ($this->auth->hasUser()) {
58+
return Str::tinyText((string) $this->auth->id());
59+
} else {
60+
return Str::tinyText((string) $this->rememberedUser?->getAuthIdentifier()); // @phpstan-ignore cast.string
61+
}
62+
} catch (Throwable $e) {
63+
$this->reportResolvingUserIdException($e);
64+
65+
return '';
4466
}
4567
});
4668
}
@@ -56,18 +78,26 @@ public function details(): ?array
5678
return null;
5779
}
5880

81+
try {
82+
$id = $user->getAuthIdentifier();
83+
} catch (Throwable $e) {
84+
$this->reportResolvingUserIdException($e);
85+
86+
return null;
87+
}
88+
5989
$resolver = call_user_func($this->userDetailsResolverResolver);
6090

6191
if ($resolver === null) {
6292
return [
63-
'id' => $user->getAuthIdentifier(),
93+
'id' => $id,
6494
'name' => $user->name ?? '',
6595
'username' => $user->email ?? '',
6696
];
6797
}
6898

6999
return [
70-
'id' => $user->getAuthIdentifier(),
100+
'id' => $id,
71101
...$resolver($user),
72102
];
73103
}
@@ -80,5 +110,19 @@ public function remember(Authenticatable $user): void
80110
public function flush(): void
81111
{
82112
$this->rememberedUser = null;
113+
$this->alreadyReportedResolvingUserIdException = false;
114+
}
115+
116+
private function reportResolvingUserIdException(Throwable $e): void
117+
{
118+
if ($this->alreadyReportedResolvingUserIdException) {
119+
return;
120+
}
121+
122+
$this->alreadyReportedResolvingUserIdException = true;
123+
124+
$report = call_user_func($this->reportResolver);
125+
126+
$report($e, true);
83127
}
84128
}

tests/FakeIngest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPUnit\Framework\Assert;
1212

1313
use function collect;
14+
use function dd;
1415
use function explode;
1516
use function is_array;
1617
use function json_decode;
@@ -109,6 +110,13 @@ public function assertLatestWrite(string|array|Closure $key, mixed $expected = n
109110
return $this->assertWrite($this->streams->count() - 1, $key, $expected);
110111
}
111112

113+
public function assertLatestWriteRecordCount(int $count): self
114+
{
115+
Assert::assertCount($count, $this->decodedWrites()->last() ?? []);
116+
117+
return $this;
118+
}
119+
112120
public function latestWriteAsString(): ?string
113121
{
114122
return $this->streams->last()?->value;
@@ -142,4 +150,9 @@ public function __set(string $name, mixed $value): void
142150
{
143151
$this->ingest->{$name} = $value;
144152
}
153+
154+
public function dd(): never
155+
{
156+
dd($this->decodedWrites()->all());
157+
}
145158
}

tests/Feature/Sensors/UserSensorTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Carbon\CarbonImmutable;
77
use Illuminate\Auth\GenericUser;
88
use Illuminate\Contracts\Auth\Authenticatable;
9+
use Illuminate\Support\Facades\Auth;
10+
use Illuminate\Support\Facades\DB;
911
use Illuminate\Support\Facades\Route;
1012
use Laravel\Nightwatch\Facades\Nightwatch;
1113
use Tests\TestCase;
@@ -220,4 +222,30 @@ public function test_it_it_captures_the_user_id_even_when_excluded_from_the_nigh
220222
'username' => '',
221223
]]);
222224
}
225+
226+
public function test_it_gracefully_handles_exceptions_while_resolving_user_ids(): void
227+
{
228+
$ingest = $this->fakeIngest();
229+
Route::get('/login', function () {
230+
DB::statement('select * from users');
231+
232+
Auth::setUser(new GenericUser([]));
233+
234+
DB::statement('select * from users');
235+
236+
return 'ok';
237+
});
238+
239+
$response = $this->get('/login');
240+
241+
$response->assertOk();
242+
$response->assertContent('ok');
243+
$ingest->assertLatestWriteRecordCount(4);
244+
$ingest->assertLatestWrite('exception:0.message', 'Undefined array key "id"');
245+
$ingest->assertLatestWrite('exception:0.class', 'ErrorException');
246+
$ingest->assertLatestWrite('exception:0.handled', true);
247+
$ingest->assertLatestWrite('query:0.user', '');
248+
$ingest->assertLatestWrite('query:1.user', '');
249+
$ingest->assertLatestWrite('request:0.user', '');
250+
}
223251
}

tests/Unit/UserProviderTest.php

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
use Illuminate\Auth\GenericUser;
66
use Illuminate\Support\Facades\Auth;
77
use Laravel\Nightwatch\UserProvider;
8+
use RuntimeException;
89
use Tests\TestCase;
910

11+
use function collect;
12+
use function json_encode;
1013
use function str_repeat;
1114
use function strlen;
1215

@@ -17,15 +20,15 @@ public function test_it_limits_the_length_of_the_user_identifier(): void
1720
Auth::login(new GenericUser([
1821
'id' => str_repeat('x', 1000),
1922
]));
20-
$provider = new UserProvider($this->app['auth'], fn () => []);
23+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => fn () => null);
2124

2225
$this->assertSame(1000, strlen(Auth::id()));
2326
$this->assertSame($provider->id(), str_repeat('x', 255));
2427
}
2528

2629
public function test_it_can_lazily_retrieve_the_user(): void
2730
{
28-
$provider = new UserProvider($this->app['auth'], fn () => []);
31+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => fn () => null);
2932

3033
$id = $provider->id();
3134

@@ -38,11 +41,120 @@ public function test_it_can_lazily_retrieve_the_user(): void
3841

3942
public function test_it_can_remember_an_authenticated_user_and_limits_the_length_of_their_identifier(): void
4043
{
41-
$provider = new UserProvider($this->app['auth'], fn () => []);
44+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => fn () => null);
4245
$provider->remember($user = new GenericUser([
4346
'id' => str_repeat('x', 1000),
4447
]));
4548

4649
$this->assertSame(str_repeat('x', 255), $provider->id()->jsonSerialize());
4750
}
51+
52+
public function test_it_only_reports_exceptions_occurring_while_resolving_user_ids_once_before_user_is_available(): void
53+
{
54+
$exceptions = collect();
55+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => function ($e) use ($exceptions) {
56+
$exceptions[] = $e;
57+
});
58+
59+
$ids = [
60+
$provider->id(),
61+
$provider->id(),
62+
];
63+
64+
$this->app['auth']->setUser(new class([]) extends GenericUser
65+
{
66+
public function getAuthIdentifier()
67+
{
68+
throw new RuntimeException('Whoops!');
69+
}
70+
});
71+
72+
json_encode($ids);
73+
74+
$this->assertCount(1, $exceptions);
75+
$this->assertSame('Whoops!', $exceptions[0]->getMessage());
76+
}
77+
78+
public function test_it_only_reports_exceptions_occurring_while_resolving_user_ids_once_after_user_is_available(): void
79+
{
80+
$exceptions = collect();
81+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => function ($e) use ($exceptions) {
82+
$exceptions[] = $e;
83+
});
84+
85+
$this->app['auth']->setUser(new class([]) extends GenericUser
86+
{
87+
public function getAuthIdentifier()
88+
{
89+
throw new RuntimeException('Whoops!');
90+
}
91+
});
92+
93+
json_encode([
94+
$provider->id(),
95+
$provider->id(),
96+
]);
97+
98+
$this->assertCount(1, $exceptions);
99+
$this->assertSame('Whoops!', $exceptions[0]->getMessage());
100+
}
101+
102+
public function test_it_only_reports_exceptions_occurring_while_resolving_user_ids_once_regardless_of_where_resolving_occurs(): void
103+
{
104+
$exceptions = collect();
105+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => function ($e) use ($exceptions) {
106+
$exceptions[] = $e;
107+
});
108+
109+
$ids = [
110+
$provider->id(),
111+
$provider->id(),
112+
];
113+
114+
$this->app['auth']->setUser(new class([]) extends GenericUser
115+
{
116+
public function getAuthIdentifier()
117+
{
118+
throw new RuntimeException('Whoops!');
119+
}
120+
});
121+
122+
json_encode($ids);
123+
json_encode([
124+
$provider->id(),
125+
$provider->id(),
126+
]);
127+
128+
$this->assertCount(1, $exceptions);
129+
$this->assertSame('Whoops!', $exceptions[0]->getMessage());
130+
}
131+
132+
public function test_it_allows_reporting_exceptions_occurring_while_resolving_user_ids_again_after_flush(): void
133+
{
134+
$exceptions = collect();
135+
$provider = new UserProvider($this->app['auth'], fn () => [], fn () => function ($e) use ($exceptions) {
136+
$exceptions[] = $e;
137+
});
138+
$this->app['auth']->setUser(new class([]) extends GenericUser
139+
{
140+
public function getAuthIdentifier()
141+
{
142+
throw new RuntimeException('Whoops!');
143+
}
144+
});
145+
146+
json_encode($provider->id());
147+
json_encode($provider->id());
148+
149+
$this->assertCount(1, $exceptions);
150+
$this->assertSame('Whoops!', $exceptions[0]->getMessage());
151+
152+
$provider->flush();
153+
154+
json_encode($provider->id());
155+
json_encode($provider->id());
156+
157+
$this->assertCount(2, $exceptions);
158+
$this->assertSame('Whoops!', $exceptions[1]->getMessage());
159+
}
48160
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy