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)