diff --git a/web/src/logos/logo-info.test.ts b/web/src/logos/logo-info.test.ts index e893000..1bbf458 100644 --- a/web/src/logos/logo-info.test.ts +++ b/web/src/logos/logo-info.test.ts @@ -388,6 +388,62 @@ describe('Löschen durch den Hochlader', () => { 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(); + 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 () => { user = 'u9'; fx.deleteLogo.mockRejectedValueOnce(new TypeError('offline')); diff --git a/web/src/logos/logo-info.ts b/web/src/logos/logo-info.ts index dcaa532..86001d9 100644 --- a/web/src/logos/logo-info.ts +++ b/web/src/logos/logo-info.ts @@ -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. 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 if (signal.aborted) return; host.removeLogo(logo.id); diff --git a/web/src/logos/logo-picker.test.ts b/web/src/logos/logo-picker.test.ts index 93da882..01689ce 100644 --- a/web/src/logos/logo-picker.test.ts +++ b/web/src/logos/logo-picker.test.ts @@ -1491,6 +1491,23 @@ describe('Löschen (mit Raster und Auswahl)', () => { 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 () => { await mount(); picker.select('b');