Files
AirCardIOS/web/src/i18n/api-messages.test.ts
T
ClaudeandClaude Opus 5.5 3051b3b3f9 feat(logos): Datenschicht, API-Aufrufe und übersetzbare API-Fehler
- logos/logos.ts (Muster community/gallery.ts): CommunityLogo, toLogo, logoQueries (Beliebt/Neu, Admin-Filter Gemeldet und
  Ausgeblendet, Cursor), listLogos (nextCursor nur bei voller Seite von 24), getLogo (ungültige ID oder 404 -> null),
  myHearts (eigene Herzen einer Seite, ohne IDs keine Anfrage), listLogoReports (ohne Melder-Hash), loadLogoBlob.
- logos/logo-api.ts: uploadLogo, setHeart, reportLogo (ohne Anmeldung), deleteLogo, setLogoVisibility über apiPost.
  uploadLogo behandelt das Limit (429 tooManyLogos) wie publishCard als eindeutige Ablehnung (die hochgeladene Datei darf
  aufgeräumt werden); eine Antwort ohne id (und bei setHeart ohne Zustand und Zähler) ist 503 mit offenem Ausgang.
- account/appwrite.ts: IDS um logos, logoLikes, logoReports und logoBucket.
- community/api.ts: ApiError.serverCode (der Schlüssel aus {error, code}; code bleibt der HTTP-Status, message der deutsche
  Text). Client-Fehler tragen unavailable (auch 502/503/504 vom Proxy, vor dem Lesen des Bodys), generic, jwtUnsupported,
  alreadyPublished, tooManyPublications. Neu: unavailableError() und apiErrorText(err). Die deutschen Texte stehen einmal
  in api-messages.ts, die Vergleiche in publishCard laufen unverändert über err.message.
- i18n/api-messages.ts: API_MESSAGES (alle 33 Texte von MSG plus die Client-Codes generic und jwtUnsupported) und
  apiMessage: bekannter Code -> t(Text); unbekannt: Deutsch der Servertext, Englisch eine Meldung je Status (400, 403, 404,
  sonst allgemein). Der Code badRequest trägt auch die dynamischen Prüftexte des Dienstes, Deutsch zeigt dann den
  genaueren Servertext.
- i18n/en.ts: englische Texte zu allen API_MESSAGES.
- Tests: api-messages.test.ts lädt MSG aus deploy/api/common.js (createRequire) und prüft den Gleichlauf in beide
  Richtungen; logos.test.ts, logo-api.test.ts und neue Fälle in api.test.ts (der Helfer freshApi stellt die frische
  i18n-Instanz auf Deutsch). 156 neue Tests, die bisherigen unverändert grün (1285 gesamt).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:59:08 +00:00

125 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRequire } from 'node:module';
import { describe, expect, it } from 'vitest';
import { API_MESSAGES, apiMessage } from './api-messages';
import { EN } from './en';
import { setLang } from './i18n';
// MSG, alle Fehlertexte der API, liegt im Dienst (deploy/api/common.js, CommonJS ohne Abhängigkeiten); der Test lädt es von dort.
const { MSG } = createRequire(import.meta.url)('../../../deploy/api/common.js') as { MSG: Record<string, string> };
const GENERIC_EN = 'Something went wrong. Please try again.';
describe('Gleichlauf mit MSG im API-Dienst (deploy/api/common.js)', () => {
it('der Dienst liefert seine Texte (sonst bestünde der Vergleich leer)', () => {
expect(Object.keys(MSG).length).toBeGreaterThan(30);
expect(MSG.alreadyPublished).toBe('Diese Karte ist schon veröffentlicht.');
});
it('jeder Schlüssel von MSG steht mit demselben deutschen Text in API_MESSAGES', () => {
const different = Object.keys(MSG).filter((key) => API_MESSAGES[key] !== MSG[key]);
expect(different).toEqual([]);
});
it('darüber hinaus hat API_MESSAGES nur die Client-Codes generic und jwtUnsupported', () => {
const extra = Object.keys(API_MESSAGES).filter((key) => !Object.hasOwn(MSG, key));
expect(extra.sort()).toEqual(['generic', 'jwtUnsupported']);
});
it('jeder Text hat eine englische Übersetzung, die vom Deutschen abweicht', () => {
const untranslated = Object.values(API_MESSAGES).filter((text) => !Object.hasOwn(EN, text) || EN[text] === text);
expect(untranslated).toEqual([]);
});
});
describe('apiMessage: bekannter Code', () => {
it('Deutsch: der Text des Codes', () => {
expect(apiMessage('alreadyPublished', 409, 'Diese Karte ist schon veröffentlicht.')).toBe(
'Diese Karte ist schon veröffentlicht.',
);
expect(apiMessage('logoGone', 404, 'Dieses Logo ist nicht (mehr) verfügbar.')).toBe('Dieses Logo ist nicht (mehr) verfügbar.');
});
it('Englisch: die Übersetzung des Codes', () => {
setLang('en');
expect(apiMessage('alreadyPublished', 409, 'Diese Karte ist schon veröffentlicht.')).toBe('This card is already published.');
expect(apiMessage('logoGone', 404, 'Dieses Logo ist nicht (mehr) verfügbar.')).toBe('This logo is no longer available.');
expect(apiMessage('unavailable', 503, 'Der Dienst ist gerade nicht erreichbar.')).toBe('The service is currently unavailable.');
});
it('jeder Code in beiden Sprachen: deutsch wie im Wörterbuch, englisch laut en.ts', () => {
for (const [code, text] of Object.entries(API_MESSAGES)) {
setLang('de');
expect(apiMessage(code, 400, text)).toBe(text);
setLang('en');
expect(apiMessage(code, 400, text)).toBe(EN[text]);
}
});
it('Englisch: der Code entscheidet, nicht der mitgelieferte Text und nicht der Status', () => {
setLang('en');
expect(apiMessage('logoGone', 500, 'irgendein anderer Text')).toBe('This logo is no longer available.');
expect(apiMessage('logoGone', 404, '')).toBe('This logo is no longer available.');
});
it('die Sprache wird bei jedem Aufruf neu gelesen', () => {
expect(apiMessage('logoGone', 404, 'x')).toBe('Dieses Logo ist nicht (mehr) verfügbar.');
setLang('en');
expect(apiMessage('logoGone', 404, 'x')).toBe('This logo is no longer available.');
setLang('de');
expect(apiMessage('logoGone', 404, 'x')).toBe('Dieses Logo ist nicht (mehr) verfügbar.');
});
it('die Client-Codes generic und jwtUnsupported werden wie die des Dienstes übersetzt', () => {
setLang('en');
expect(apiMessage('generic', 500, 'x')).toBe(GENERIC_EN);
expect(apiMessage('jwtUnsupported', 501, 'x')).toBe(
'Token sign-in is turned off on the server – please let the site owner know.',
);
});
});
describe('apiMessage: dynamische Prüftexte (Code badRequest)', () => {
const dynamic = 'Der Titel ist zu lang (höchstens 60 Zeichen).';
it('Deutsch: der genauere Servertext statt „Ungültige Anfrage.“', () => {
expect(apiMessage('badRequest', 400, dynamic)).toBe(dynamic);
expect(apiMessage('badRequest', 400, 'Ungültige Anfrage.')).toBe('Ungültige Anfrage.');
});
it('Englisch: der allgemeine Text des Codes (der deutsche Prüftext lässt sich nicht übersetzen)', () => {
setLang('en');
expect(apiMessage('badRequest', 400, dynamic)).toBe('Invalid request.');
expect(apiMessage('badRequest', 400, 'Ungültige Anfrage.')).toBe('Invalid request.');
});
});
describe('apiMessage: unbekannter Code', () => {
it.each([null, 'error', 'neuerCode', ''])('Deutsch (%j): der Servertext', (code) => {
expect(apiMessage(code, 500, 'Der Server sagt etwas.')).toBe('Der Server sagt etwas.');
expect(apiMessage(code, 400, 'Der Server sagt etwas.')).toBe('Der Server sagt etwas.');
});
it.each([
[400, 'Invalid request.'],
[403, 'You are not allowed to do that.'],
[404, 'Not found.'],
[401, GENERIC_EN],
[409, GENERIC_EN],
[429, GENERIC_EN],
[500, GENERIC_EN],
[503, GENERIC_EN],
] as const)('Englisch: Status %i → „%s“', (status, expected) => {
setLang('en');
for (const code of [null, 'error', 'neuerCode']) expect(apiMessage(code, status, 'Der Server sagt etwas.')).toBe(expected);
});
it.each(['constructor', 'toString', '__proto__', 'hasOwnProperty'])(
'Namen aus Object.prototype (%s) sind kein Code',
(code) => {
expect(apiMessage(code, 500, 'Servertext')).toBe('Servertext');
setLang('en');
expect(apiMessage(code, 500, 'Servertext')).toBe(GENERIC_EN);
},
);
});