fix(logos): 404 „logoGone“ beim Löschen gilt als Erfolg

Schlussreview F5. Geht die Antwort einer Löschanfrage verloren und der Nutzer klickt noch einmal auf „Löschen“, antwortet der Dienst mit
404 `logoGone` (das Logo ist ja schon weg). Die Infozeile zeigte dann „Dieses Logo ist nicht (mehr) verfügbar.“ als Fehler, die Kachel
blieb stehen.

- Dieser Fehler (ApiError mit Status 404 und Schlüssel `logoGone`) gilt wie ein gelungenes Löschen: forgetLogo, die Kachel verschwindet,
  eine Auswahl wird aufgehoben (onSelect(null)), kein Fehlertext – wie bei „Zurückziehen“ einer schon zurückgezogenen Karte.
- Jeder andere Fehler bleibt einer, auch ein 404 mit anderem oder ohne Schlüssel (etwa ein Dienst, der die Anfrage nicht kennt).
- Tests in logo-info.test.ts (Hochlader, Admin, Seite verlassen, Fehler, die Fehler bleiben) und im Picker mit Raster und Auswahl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 01:19:17 +00:00
1 parent 4aedb1d322
commit df1cf29d42
3 files changed
+80 -1

No files matched your search

+56
View File
@@ -388,6 +388,62 @@ describe('Löschen durch den Hochlader', () => {
expect(host.removeLogo).toHaveBeenCalledExactlyOnceWith('a'); expect(host.removeLogo).toHaveBeenCalledExactlyOnceWith('a');
}); });
it('404 „logoGone“ (das Logo gibt es schon nicht mehr, etwa weil die Antwort einer ersten Anfrage verloren ging): gilt als Erfolg wie ein gelungenes Löschen', async () => {
user = 'u9';
const order: string[] = [];
fx.deleteLogo.mockRejectedValueOnce(new ApiError(404, 'Dieses Logo ist nicht (mehr) verfügbar.', true, 'logoGone'));
mocks.forgetLogo.mockImplementation(() => void order.push('forget'));
host.removeLogo.mockImplementation(() => void order.push('remove'));
await mount();
button('Löschen').click();
await flush();
expect(order).toEqual(['forget', 'remove']);
expect(mocks.forgetLogo).toHaveBeenCalledExactlyOnceWith('a');
expect(host.removeLogo).toHaveBeenCalledExactlyOnceWith('a');
expect(message()).toBe(''); // kein Fehlertext
});
it('404 „logoGone“ gilt für den Admin ebenso, auch bei einem fremden Logo', async () => {
admin = true;
fx.deleteLogo.mockRejectedValueOnce(new ApiError(404, 'Dieses Logo ist nicht (mehr) verfügbar.', true, 'logoGone'));
await mount();
button('Löschen').click();
await flush();
expect(mocks.forgetLogo).toHaveBeenCalledExactlyOnceWith('a');
expect(host.removeLogo).toHaveBeenCalledExactlyOnceWith('a');
expect(message()).toBe('');
});
it('404 „logoGone“ nach dem Verlassen der Seite: der Zwischenspeicher vergisst das Logo trotzdem, die Kachel wird nicht mehr angefasst', async () => {
user = 'u9';
const request = deferred<void>();
fx.deleteLogo.mockReturnValue(request.promise);
await mount();
button('Löschen').click();
await flush();
abort.abort();
request.reject(new ApiError(404, 'Dieses Logo ist nicht (mehr) verfügbar.', true, 'logoGone'));
await flush();
expect(mocks.forgetLogo).toHaveBeenCalledExactlyOnceWith('a');
expect(host.removeLogo).not.toHaveBeenCalled();
});
it.each([
['ein 404 ohne diesen Schlüssel (etwa ein Dienst, der die Anfrage nicht kennt)', new ApiError(404, 'Etwas ist schiefgelaufen. Bitte nochmal versuchen.', true, 'generic')],
['ein 404 ohne Schlüssel', new ApiError(404, 'Nicht gefunden', true)],
['logoGone mit einem anderen Status', new ApiError(500, 'Dieses Logo ist nicht (mehr) verfügbar.', false, 'logoGone')],
['ein 403', new ApiError(403, 'Dazu hast du keine Berechtigung.', true, 'forbidden')],
])('%s bleibt ein Fehler: Text in der Meldezeile, nichts wird vergessen oder entfernt', async (_name, error) => {
user = 'u9';
fx.deleteLogo.mockRejectedValueOnce(error);
await mount();
button('Löschen').click();
await flush();
expect(message()).not.toBe('');
expect(mocks.forgetLogo).not.toHaveBeenCalled();
expect(host.removeLogo).not.toHaveBeenCalled();
});
it('ein Fehler anderer Art (Netz) bekommt den allgemeinen Text', async () => { it('ein Fehler anderer Art (Netz) bekommt den allgemeinen Text', async () => {
user = 'u9'; user = 'u9';
fx.deleteLogo.mockRejectedValueOnce(new TypeError('offline')); fx.deleteLogo.mockRejectedValueOnce(new TypeError('offline'));
+7 -1
View File
@@ -208,7 +208,13 @@ export function createLogoInfo(host: LogoInfoHost, deps: LogoInfoDeps, signal: A
); );
// Während der Rückfrage kann die Seite verlassen oder die Sitzung gewechselt haben: dann nichts mehr senden. // Während der Rückfrage kann die Seite verlassen oder die Sitzung gewechselt haben: dann nichts mehr senden.
if (!confirmed || signal.aborted || !mayDelete(logo)) return; if (!confirmed || signal.aborted || !mayDelete(logo)) return;
await deps.deleteLogo(logo.id); try {
await deps.deleteLogo(logo.id);
} catch (err) {
// Das Logo gibt es schon nicht mehr (die Antwort einer ersten Anfrage ging verloren und der Nutzer versucht es noch einmal, oder es
// wurde anderswo gelöscht): Ziel erreicht. Nur der Schlüssel `logoGone` zählt, jedes andere 404 ist ein echter Fehler.
if (!(err instanceof ApiError && err.code === 404 && err.serverCode === 'logoGone')) throw err;
}
forgetLogo(logo.id); // auch nach dem Verlassen der Seite: der Zwischenspeicher darf das gelöschte Logo nicht behalten forgetLogo(logo.id); // auch nach dem Verlassen der Seite: der Zwischenspeicher darf das gelöschte Logo nicht behalten
if (signal.aborted) return; if (signal.aborted) return;
host.removeLogo(logo.id); host.removeLogo(logo.id);
+17
View File
@@ -1491,6 +1491,23 @@ describe('Löschen (mit Raster und Auswahl)', () => {
expect(revoked).toContain(created[1]); // das Bild der Kachel ist freigegeben expect(revoked).toContain(created[1]); // das Bild der Kachel ist freigegeben
}); });
it('404 „logoGone“ (es gibt das Logo schon nicht mehr): gilt als gelöscht – Kachel weg, forgetLogo, Auswahl aufgehoben, kein Fehlertext', async () => {
fx.deleteLogo.mockRejectedValue(new ApiError(404, 'Dieses Logo ist nicht (mehr) verfügbar.', true, 'logoGone'));
await mount();
picker.select('b');
infoButton('Löschen').click();
await flush();
expect(fx.deleteLogo).toHaveBeenCalledExactlyOnceWith('b');
expect(mocks.forgetLogo).toHaveBeenCalledExactlyOnceWith('b');
expect(names()).toEqual(['Kein Logo', 'Logo A', 'Logo C']);
expect(fx.onSelect).toHaveBeenCalledExactlyOnceWith(null);
expect(picker.selected()).toBeNull();
expect(infoMessage()).toBe('');
expect(alertText()).toBe('');
expect(infoLine()).toBe('');
expect(revoked).toContain(created[1]); // das Bild der Kachel ist freigegeben
});
it('der Editor übernimmt onSelect(null) mit select(null): „Kein Logo“ ist gewählt, nichts bleibt hängen', async () => { it('der Editor übernimmt onSelect(null) mit select(null): „Kein Logo“ ist gewählt, nichts bleibt hängen', async () => {
await mount(); await mount();
picker.select('b'); picker.select('b');