fix: Registrierung — Platzhalter serverseitig übersetzt statt als Alpine-Ausdruck (GlitchTip browser-pressekonto 15), Wächtertest; Promise-Ablehnungen mit Typ und Inhalt (10); vertippte artisan-Befehle nicht melden (server-pressekonto 9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-06 13:12:29 +00:00
parent a90f24a43f
commit 16af3740bc
6 changed files with 88 additions and 3 deletions

View file

@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Route;
use Laravel\Sanctum\Http\Middleware\CheckAbilities; use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
use Sentry\Laravel\Integration; use Sentry\Laravel\Integration;
use Symfony\Component\Console\Exception\CommandNotFoundException;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -162,4 +163,8 @@ return Application::configure(basePath: dirname(__DIR__))
// Validierung, Auth) gilt weiter — diese Fälle meldet // Validierung, Auth) gilt weiter — diese Fälle meldet
// ReportErrorSignals gezielt als Warnung. // ReportErrorSignals gezielt als Warnung.
Integration::handles($exceptions); Integration::handles($exceptions);
// Vertippte artisan-Befehle sind kein Anwendungsfehler (GlitchTip
// server-pressekonto, Issue 9: „config:cachels“ auf Live).
$exceptions->dontReport(CommandNotFoundException::class);
})->create(); })->create();

View file

@ -87,6 +87,27 @@ function loadSdk() {
return sdk; return sdk;
} }
/**
* Promise-Ablehnungen mit Objekt statt Error kamen als „Error: [object Object]“
* an (GlitchTip browser-pressekonto, Issue 10) — Typ und Inhalt mitschicken,
* damit die Quelle erkennbar ist.
*/
function rejectionToError(reason) {
if (reason instanceof Error) {
return reason;
}
const kind = reason?.constructor?.name ?? typeof reason;
let text;
try {
text = typeof reason === 'string' ? reason : JSON.stringify(reason);
} catch {
text = String(reason);
}
return new Error(`Unbehandelte Ablehnung (${kind}): ${String(text ?? '').slice(0, 300)}`);
}
function capture(error) { function capture(error) {
loadSdk() loadSdk()
.then((Sentry) => Sentry.captureException(error)) .then((Sentry) => Sentry.captureException(error))
@ -102,6 +123,6 @@ if (config?.dsn) {
}); });
window.addEventListener('unhandledrejection', (event) => { window.addEventListener('unhandledrejection', (event) => {
capture(event.reason instanceof Error ? event.reason : new Error(String(event.reason))); capture(rejectionToError(event.reason));
}); });
} }

View file

@ -88,7 +88,7 @@ new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Konto erstell
autofocus autofocus
autocomplete="name" autocomplete="name"
class="field-input" class="field-input"
:placeholder="__('Vor- und Nachname')" placeholder="{{ __('Vor- und Nachname') }}"
@error('name') aria-invalid="true" @enderror @error('name') aria-invalid="true" @enderror
/> />
@error('name') @error('name')

View file

@ -11,7 +11,11 @@ test('registration screen can be rendered', function () {
/** @var TestCase $this */ /** @var TestCase $this */
$response = $this->get('/register'); $response = $this->get('/register');
$response->assertStatus(200); // Platzhalter serverseitig übersetzt, nicht als Alpine-Ausdruck
// (ReferenceError „__ is not defined“, GlitchTip 06.10.2026).
$response->assertStatus(200)
->assertSee('placeholder="Vor- und Nachname"', false)
->assertDontSee(':placeholder="__(', false);
}); });
test('new users register inactive and are sent to the verification notice', function () { test('new users register inactive and are sent to the verification notice', function () {

View file

@ -0,0 +1,46 @@
<?php
use Illuminate\Support\Facades\File;
/**
* `:attr="__('…')"` ist nur an Blade-Komponenten (`<x-…>`, `<flux:…>`)
* ein PHP-Ausdruck. An einem normalen HTML-Element reicht Blade das
* Attribut unverändert an Alpine durch, und der Browser wirft
* „ReferenceError: __ is not defined“ — so geschehen auf /register
* (GlitchTip browser-pressekonto, Issue 15, 06.10.2026). Für HTML-Elemente
* gilt: `attr="{{ __('…') }}"`.
*/
function alpineTranslationBindingPattern(): string
{
// HTML-Tag (keine Komponente), beliebige Attribute auch über mehrere
// Zeilen, dann ein Alpine-Binding, dessen Ausdruck mit __( beginnt.
return '/<(?!x-|flux:|livewire:|\/)([a-z][a-z0-9-]*)\b((?:[^>"]|"[^"]*")*?)\s(:[a-zA-Z.-]+|x-bind:[a-zA-Z.-]+)="\s*__\(/s';
}
test('no html element binds an alpine attribute to the php translation helper', function () {
$pattern = alpineTranslationBindingPattern();
$offenders = [];
foreach (File::allFiles(resource_path('views')) as $view) {
if (! str_ends_with($view->getFilename(), '.blade.php')) {
continue;
}
$contents = File::get($view->getRealPath());
preg_match_all($pattern, $contents, $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[3] as [$attribute, $offset]) {
$offenders[] = sprintf('%s:%d %s', $view->getRelativePathname(), substr_count($contents, "\n", 0, $offset) + 1, $attribute);
}
}
expect($offenders)->toBe([], "Alpine-Binding mit __() an einem HTML-Element — auf attr=\"{{ __('…') }}\" umstellen:\n".implode("\n", $offenders));
});
test('the guard catches the pattern that broke the registration', function () {
$pattern = alpineTranslationBindingPattern();
expect(preg_match($pattern, "<input\n wire:model=\"name\"\n :placeholder=\"__('Vor- und Nachname')\"\n/>"))->toBe(1)
->and(preg_match($pattern, "<x-web.page-header\n :title=\"__('static.faq.title')\"\n/>"))->toBe(0);
});

View file

@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
use Sentry\Event; use Sentry\Event;
use Sentry\ExceptionDataBag; use Sentry\ExceptionDataBag;
use Sentry\SentrySdk; use Sentry\SentrySdk;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Tests\Support\FakeGlitchTip; use Tests\Support\FakeGlitchTip;
/** /**
@ -35,6 +36,14 @@ test('exceptions laravel does not report stay out of glitchtip', function () {
expect($glitchTip->events)->toBe([]); expect($glitchTip->events)->toBe([]);
}); });
test('a mistyped artisan command is not reported', function () {
$glitchTip = FakeGlitchTip::install();
report(new CommandNotFoundException('Command "config:cachels" is not defined.'));
expect($glitchTip->events)->toBe([]);
});
test('request data loses tokens, signatures and bodies', function () { test('request data loses tokens, signatures and bodies', function () {
$scrubbed = ErrorReportScrubber::scrubRequest([ $scrubbed = ErrorReportScrubber::scrubRequest([
'url' => 'https://pressekonto.test/newsroom/claim/5?signature=abc123&expires=1700000000&src=mail_publish', 'url' => 'https://pressekonto.test/newsroom/claim/5?signature=abc123&expires=1700000000&src=mail_publish',