- 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>
466 lines
17 KiB
TypeScript
466 lines
17 KiB
TypeScript
import { AppwriteException, Query } from 'appwrite';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
listRows: vi.fn(),
|
|
getRow: vi.fn(),
|
|
fetchFile: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../account/appwrite', () => ({
|
|
IDS: {
|
|
database: 'db',
|
|
table: 'cards-table',
|
|
bucket: 'cards-bucket',
|
|
gallery: 'gallery-table',
|
|
reports: 'reports-table',
|
|
galleryBucket: 'gallery-bucket',
|
|
logos: 'logos-table',
|
|
logoLikes: 'likes-table',
|
|
logoReports: 'logo-reports-table',
|
|
logoBucket: 'logo-bucket',
|
|
},
|
|
account: {},
|
|
client: {},
|
|
storage: {},
|
|
APPWRITE_ENDPOINT: 'https://aircard.test/v1',
|
|
tables: { listRows: mocks.listRows, getRow: mocks.getRow },
|
|
}));
|
|
|
|
// ID_PATTERN & Co. bleiben echt; nur das Laden der Datei geht nicht ins Netz.
|
|
vi.mock('../account/cards', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../account/cards')>()),
|
|
fetchFile: mocks.fetchFile,
|
|
}));
|
|
|
|
import {
|
|
LOGO_PAGE_SIZE,
|
|
getLogo,
|
|
listLogoReports,
|
|
listLogos,
|
|
loadLogoBlob,
|
|
logoQueries,
|
|
myHearts,
|
|
toLogo,
|
|
type CommunityLogo,
|
|
} from './logos';
|
|
|
|
const row = (n: number, patch: Record<string, unknown> = {}) => ({
|
|
$id: `l${n}`,
|
|
$createdAt: `2026-10-0${(n % 9) + 1}T10:00:00.000Z`,
|
|
name: `Logo ${n}`,
|
|
displayName: 'Kenso',
|
|
fileId: `l${n}`,
|
|
ownerId: 'u1',
|
|
mono: false,
|
|
likeCount: n,
|
|
hidden: false,
|
|
reportCount: 0,
|
|
...patch,
|
|
});
|
|
|
|
const notFound = (type = 'row_not_found') => Object.assign(new Error('Row not found'), { code: 404, type });
|
|
|
|
beforeEach(() => {
|
|
mocks.listRows.mockReset();
|
|
mocks.getRow.mockReset();
|
|
mocks.fetchFile.mockReset();
|
|
});
|
|
|
|
describe('IDS', () => {
|
|
it('kennt die Logo-Tabellen und den Logo-Bucket (Namen wie im Setup-Skript)', async () => {
|
|
vi.stubEnv('VITE_APPWRITE_ENDPOINT', 'https://aircard.test/v1');
|
|
try {
|
|
const { IDS } = await vi.importActual<typeof import('../account/appwrite')>('../account/appwrite');
|
|
expect(IDS).toMatchObject({
|
|
logos: 'logos',
|
|
logoLikes: 'logo_likes',
|
|
logoReports: 'logo_reports',
|
|
logoBucket: 'logo-images',
|
|
});
|
|
expect(IDS.database).toBe('aircard'); // und das Bestehende bleibt
|
|
expect(IDS.gallery).toBe('gallery');
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('logoQueries', () => {
|
|
it('Beliebt: nur sichtbare Logos, meiste Herzen zuerst, bei Gleichstand die neuesten, 24 pro Seite', () => {
|
|
expect(logoQueries({ sort: 'popular' })).toEqual([
|
|
Query.equal('hidden', false),
|
|
Query.orderDesc('likeCount'),
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(24),
|
|
]);
|
|
expect(LOGO_PAGE_SIZE).toBe(24);
|
|
});
|
|
|
|
it('die Abfragen sind genau die Appwrite-JSON-Strings', () => {
|
|
expect(logoQueries({ sort: 'popular' })).toEqual([
|
|
JSON.stringify({ method: 'equal', attribute: 'hidden', values: [false] }),
|
|
JSON.stringify({ method: 'orderDesc', attribute: 'likeCount' }),
|
|
JSON.stringify({ method: 'orderDesc', attribute: '$createdAt' }),
|
|
JSON.stringify({ method: 'limit', values: [24] }),
|
|
]);
|
|
});
|
|
|
|
it('Neu: neueste zuerst, ohne Sortierung nach likeCount', () => {
|
|
const queries = logoQueries({ sort: 'new' });
|
|
expect(queries).toEqual([Query.equal('hidden', false), Query.orderDesc('$createdAt'), Query.limit(24)]);
|
|
expect(queries.join('')).not.toContain('likeCount');
|
|
});
|
|
|
|
it("Admin-Filter 'all' ist die öffentliche Ansicht", () => {
|
|
expect(logoQueries({ sort: 'popular', filter: 'all' })).toEqual(logoQueries({ sort: 'popular' }));
|
|
expect(logoQueries({ sort: 'new', filter: 'all' })).toEqual(logoQueries({ sort: 'new' }));
|
|
});
|
|
|
|
it("Admin-Filter 'reported': reportCount > 0, ohne hidden-Bedingung (gemeldet und schon ausgeblendet)", () => {
|
|
const queries = logoQueries({ sort: 'new', filter: 'reported' });
|
|
expect(queries).toEqual([Query.greaterThan('reportCount', 0), Query.orderDesc('$createdAt'), Query.limit(24)]);
|
|
expect(queries.join('')).not.toContain('hidden');
|
|
});
|
|
|
|
it("Admin-Filter 'hidden': nur ausgeblendete Logos", () => {
|
|
expect(logoQueries({ sort: 'new', filter: 'hidden' })).toEqual([
|
|
Query.equal('hidden', true),
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(24),
|
|
]);
|
|
});
|
|
|
|
it('Filter und Sortierung sind unabhängig: Gemeldet nach Beliebtheit', () => {
|
|
expect(logoQueries({ sort: 'popular', filter: 'reported' })).toEqual([
|
|
Query.greaterThan('reportCount', 0),
|
|
Query.orderDesc('likeCount'),
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(24),
|
|
]);
|
|
});
|
|
|
|
it('mit Cursor: zusätzlich cursorAfter(letzte ID), als letzte Abfrage', () => {
|
|
expect(logoQueries({ sort: 'popular', cursor: 'l24' })).toEqual([
|
|
Query.equal('hidden', false),
|
|
Query.orderDesc('likeCount'),
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(24),
|
|
Query.cursorAfter('l24'),
|
|
]);
|
|
expect(logoQueries({ sort: 'new', filter: 'hidden', cursor: 'l5' })).toEqual([
|
|
Query.equal('hidden', true),
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(24),
|
|
Query.cursorAfter('l5'),
|
|
]);
|
|
});
|
|
|
|
it('ein leerer Cursor wird ignoriert', () => {
|
|
expect(logoQueries({ sort: 'new', cursor: '' })).toEqual(logoQueries({ sort: 'new' }));
|
|
});
|
|
});
|
|
|
|
describe('toLogo', () => {
|
|
it('übernimmt alle Felder', () => {
|
|
expect(toLogo(row(1, { mono: true, hidden: true, reportCount: 3, likeCount: 7 }))).toEqual({
|
|
id: 'l1',
|
|
name: 'Logo 1',
|
|
displayName: 'Kenso',
|
|
fileId: 'l1',
|
|
ownerId: 'u1',
|
|
mono: true,
|
|
likeCount: 7,
|
|
hidden: true,
|
|
reportCount: 3,
|
|
createdAt: row(1).$createdAt,
|
|
} satisfies CommunityLogo);
|
|
});
|
|
|
|
it('fehlende Felder bekommen leere Standardwerte', () => {
|
|
expect(toLogo({ $id: 'l9' })).toEqual({
|
|
id: 'l9',
|
|
name: '',
|
|
displayName: '',
|
|
fileId: '',
|
|
ownerId: '',
|
|
mono: false,
|
|
likeCount: 0,
|
|
hidden: false,
|
|
reportCount: 0,
|
|
createdAt: '',
|
|
});
|
|
});
|
|
|
|
it('Felder mit falschem Typ gelten als fehlend', () => {
|
|
expect(
|
|
toLogo({
|
|
$id: 'l9',
|
|
name: 42,
|
|
displayName: null,
|
|
fileId: ['x'],
|
|
ownerId: { id: 'u1' },
|
|
mono: 'ja',
|
|
likeCount: 'viele',
|
|
hidden: 1,
|
|
reportCount: '3',
|
|
$createdAt: 5,
|
|
}),
|
|
).toEqual({
|
|
id: 'l9',
|
|
name: '',
|
|
displayName: '',
|
|
fileId: '',
|
|
ownerId: '',
|
|
mono: false,
|
|
likeCount: 0,
|
|
hidden: false,
|
|
reportCount: 0,
|
|
createdAt: '',
|
|
});
|
|
});
|
|
|
|
it('eine fehlende oder falsch typisierte $id wird leer', () => {
|
|
expect(toLogo({}).id).toBe('');
|
|
expect(toLogo({ $id: 7 }).id).toBe('');
|
|
});
|
|
|
|
it('mono und hidden sind nur bei true wahr', () => {
|
|
for (const value of [false, 'true', 1, null, undefined, 'ja']) {
|
|
const logo = toLogo({ $id: 'l1', mono: value, hidden: value });
|
|
expect(logo.mono).toBe(false);
|
|
expect(logo.hidden).toBe(false);
|
|
}
|
|
expect(toLogo({ $id: 'l1', mono: true, hidden: true })).toMatchObject({ mono: true, hidden: true });
|
|
});
|
|
|
|
it('likeCount und reportCount: nur endliche Zahlen größer als 0, sonst 0', () => {
|
|
for (const field of ['likeCount', 'reportCount'] as const) {
|
|
expect(toLogo({ $id: 'l1', [field]: 5 })[field]).toBe(5);
|
|
expect(toLogo({ $id: 'l1', [field]: 0 })[field]).toBe(0);
|
|
expect(toLogo({ $id: 'l1', [field]: -1 })[field]).toBe(0);
|
|
expect(toLogo({ $id: 'l1', [field]: -0.5 })[field]).toBe(0);
|
|
expect(toLogo({ $id: 'l1', [field]: Number.NaN })[field]).toBe(0);
|
|
expect(toLogo({ $id: 'l1', [field]: Number.POSITIVE_INFINITY })[field]).toBe(0);
|
|
expect(toLogo({ $id: 'l1', [field]: '5' })[field]).toBe(0);
|
|
expect(toLogo({ $id: 'l1', [field]: null })[field]).toBe(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('listLogos', () => {
|
|
it('fragt die Logo-Tabelle mit den Abfragen ab und liefert Logos', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 2, rows: [row(1), row(2, { mono: true })] });
|
|
const page = await listLogos({ sort: 'popular' });
|
|
expect(mocks.listRows).toHaveBeenCalledOnce();
|
|
expect(mocks.listRows.mock.calls[0][0]).toEqual({
|
|
databaseId: 'db',
|
|
tableId: 'logos-table',
|
|
queries: logoQueries({ sort: 'popular' }),
|
|
});
|
|
expect(page.logos.map((logo) => logo.id)).toEqual(['l1', 'l2']);
|
|
expect(page.logos[1]).toMatchObject({ name: 'Logo 2', mono: true, likeCount: 2 });
|
|
});
|
|
|
|
it('weniger als 24 Zeilen: keine weitere Seite (nextCursor null)', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 23, rows: Array.from({ length: 23 }, (_, i) => row(i + 1)) });
|
|
expect((await listLogos({ sort: 'new' })).nextCursor).toBeNull();
|
|
});
|
|
|
|
it('keine Zeilen: leere Liste, nextCursor null', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 0, rows: [] });
|
|
await expect(listLogos({ sort: 'new' })).resolves.toEqual({ logos: [], nextCursor: null });
|
|
});
|
|
|
|
it('genau 24 Zeilen: nextCursor ist die ID des letzten Logos', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 60, rows: Array.from({ length: 24 }, (_, i) => row(i + 1)) });
|
|
const page = await listLogos({ sort: 'popular' });
|
|
expect(page.logos).toHaveLength(24);
|
|
expect(page.nextCursor).toBe('l24');
|
|
});
|
|
|
|
it('die nächste Seite schickt den Cursor, die Admin-Ansicht den Filter mit', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 0, rows: [] });
|
|
await listLogos({ sort: 'new', cursor: 'l24', filter: 'reported' });
|
|
expect(mocks.listRows.mock.calls[0][0].queries).toEqual(logoQueries({ sort: 'new', cursor: 'l24', filter: 'reported' }));
|
|
expect(mocks.listRows.mock.calls[0][0].queries).toContain(Query.cursorAfter('l24'));
|
|
});
|
|
|
|
it('Fehler gehen durch', async () => {
|
|
const failure = Object.assign(new Error('down'), { code: 503 });
|
|
mocks.listRows.mockRejectedValue(failure);
|
|
await expect(listLogos({ sort: 'new' })).rejects.toBe(failure);
|
|
});
|
|
});
|
|
|
|
describe('getLogo', () => {
|
|
it('liefert das Logo', async () => {
|
|
mocks.getRow.mockResolvedValue(row(7, { mono: true }));
|
|
const logo = await getLogo('l7');
|
|
expect(logo).toMatchObject({ id: 'l7', mono: true });
|
|
expect(mocks.getRow).toHaveBeenCalledWith({ databaseId: 'db', tableId: 'logos-table', rowId: 'l7' });
|
|
});
|
|
|
|
it('404 (nicht vorhanden oder ausgeblendet für mich) ergibt null', async () => {
|
|
mocks.getRow.mockRejectedValue(notFound());
|
|
await expect(getLogo('l7')).resolves.toBeNull();
|
|
});
|
|
|
|
it('404 ohne Typ (z. B. Proxy) ergibt ebenfalls null', async () => {
|
|
mocks.getRow.mockRejectedValue(Object.assign(new Error('not found'), { code: 404 }));
|
|
await expect(getLogo('l7')).resolves.toBeNull();
|
|
});
|
|
|
|
it('eine fehlende Tabelle (404 table_not_found) ist ein Konfigurationsfehler, kein „Logo weg“', async () => {
|
|
const failure = notFound('table_not_found');
|
|
mocks.getRow.mockRejectedValue(failure);
|
|
await expect(getLogo('l7')).rejects.toBe(failure);
|
|
});
|
|
|
|
it.each([500, 503, 401, 429])('Fehler %i geht durch', async (code) => {
|
|
const failure = Object.assign(new Error('x'), { code, type: 'general_server_error' });
|
|
mocks.getRow.mockRejectedValue(failure);
|
|
await expect(getLogo('l7')).rejects.toBe(failure);
|
|
});
|
|
|
|
it('Netzfehler gehen durch', async () => {
|
|
const failure = new TypeError('Failed to fetch');
|
|
mocks.getRow.mockRejectedValue(failure);
|
|
await expect(getLogo('l7')).rejects.toBe(failure);
|
|
});
|
|
|
|
it.each(['', 'kaputt id', '../x', 'a/b', 'a?b', '.hidden', '-x', 'x'.repeat(37), 'ä'])(
|
|
'ungültige ID %j: null, ohne Anfrage',
|
|
async (id) => {
|
|
await expect(getLogo(id)).resolves.toBeNull();
|
|
expect(mocks.getRow).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('myHearts', () => {
|
|
it('ohne Logo-IDs: leere Menge, ohne Anfrage', async () => {
|
|
const hearts = await myHearts('u', []);
|
|
expect(hearts).toBeInstanceOf(Set);
|
|
expect(hearts.size).toBe(0);
|
|
expect(mocks.listRows).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ohne Nutzer-ID: leere Menge, ohne Anfrage', async () => {
|
|
expect((await myHearts('', ['l1'])).size).toBe(0);
|
|
expect(mocks.listRows).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fragt die eigenen Herzen für genau diese Logos ab (höchstens 100)', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 2, rows: [{ $id: 'h1', logoId: 'l1', userId: 'u' }, { $id: 'h3', logoId: 'l3', userId: 'u' }] });
|
|
const hearts = await myHearts('u', ['l1', 'l2', 'l3']);
|
|
expect(mocks.listRows).toHaveBeenCalledOnce();
|
|
expect(mocks.listRows.mock.calls[0][0]).toEqual({
|
|
databaseId: 'db',
|
|
tableId: 'likes-table',
|
|
queries: [Query.equal('userId', 'u'), Query.equal('logoId', ['l1', 'l2', 'l3']), Query.limit(100)],
|
|
});
|
|
expect([...hearts].sort()).toEqual(['l1', 'l3']);
|
|
});
|
|
|
|
it('die Abfragen sind genau die Appwrite-JSON-Strings', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 0, rows: [] });
|
|
await myHearts('u', ['l1', 'l2']);
|
|
expect(mocks.listRows.mock.calls[0][0].queries).toEqual([
|
|
JSON.stringify({ method: 'equal', attribute: 'userId', values: ['u'] }),
|
|
JSON.stringify({ method: 'equal', attribute: 'logoId', values: ['l1', 'l2'] }),
|
|
JSON.stringify({ method: 'limit', values: [100] }),
|
|
]);
|
|
});
|
|
|
|
it('ohne Herzen: leere Menge', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 0, rows: [] });
|
|
expect((await myHearts('u', ['l1'])).size).toBe(0);
|
|
});
|
|
|
|
it('Zeilen ohne brauchbare logoId werden übersprungen', async () => {
|
|
mocks.listRows.mockResolvedValue({
|
|
total: 3,
|
|
rows: [{ $id: 'h1', logoId: 'l1' }, { $id: 'h2' }, { $id: 'h3', logoId: 5 }],
|
|
});
|
|
expect([...(await myHearts('u', ['l1', 'l2']))]).toEqual(['l1']);
|
|
});
|
|
|
|
it('Fehler gehen durch', async () => {
|
|
const failure = Object.assign(new Error('down'), { code: 503 });
|
|
mocks.listRows.mockRejectedValue(failure);
|
|
await expect(myHearts('u', ['l1'])).rejects.toBe(failure);
|
|
});
|
|
});
|
|
|
|
describe('listLogoReports', () => {
|
|
it('liest die Meldungen eines Logos (höchstens 100), neueste zuerst', async () => {
|
|
mocks.listRows.mockResolvedValue({
|
|
total: 2,
|
|
rows: [
|
|
{ $id: 'r2', $createdAt: '2026-10-04T11:00:00.000Z', logoId: 'l1', reason: 'spam', reporterHash: 'abc' },
|
|
{ $id: 'r1', $createdAt: '2026-10-04T10:00:00.000Z', logoId: 'l1', reason: 'urheberrecht', reporterHash: 'def' },
|
|
],
|
|
});
|
|
const reports = await listLogoReports('l1');
|
|
expect(mocks.listRows.mock.calls[0][0]).toEqual({
|
|
databaseId: 'db',
|
|
tableId: 'logo-reports-table',
|
|
queries: [Query.equal('logoId', 'l1'), Query.orderDesc('$createdAt'), Query.limit(100)],
|
|
});
|
|
expect(reports).toEqual([
|
|
{ id: 'r2', logoId: 'l1', reason: 'spam', createdAt: '2026-10-04T11:00:00.000Z' },
|
|
{ id: 'r1', logoId: 'l1', reason: 'urheberrecht', createdAt: '2026-10-04T10:00:00.000Z' },
|
|
]);
|
|
});
|
|
|
|
it('der Melder-Hash wird nicht weitergereicht', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 1, rows: [{ $id: 'r1', logoId: 'l1', reason: 'spam', reporterHash: 'abc' }] });
|
|
expect(JSON.stringify(await listLogoReports('l1'))).not.toContain('abc');
|
|
});
|
|
|
|
it('fehlende Felder werden leer', async () => {
|
|
mocks.listRows.mockResolvedValue({ total: 1, rows: [{ $id: 'r1' }] });
|
|
await expect(listLogoReports('l1')).resolves.toEqual([{ id: 'r1', logoId: '', reason: '', createdAt: '' }]);
|
|
});
|
|
|
|
it('ungültige ID: leere Liste, ohne Anfrage', async () => {
|
|
await expect(listLogoReports('../x')).resolves.toEqual([]);
|
|
await expect(listLogoReports('')).resolves.toEqual([]);
|
|
expect(mocks.listRows).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('Fehler gehen durch', async () => {
|
|
const failure = Object.assign(new Error('forbidden'), { code: 401 });
|
|
mocks.listRows.mockRejectedValue(failure);
|
|
await expect(listLogoReports('l1')).rejects.toBe(failure);
|
|
});
|
|
});
|
|
|
|
describe('loadLogoBlob', () => {
|
|
const logo = toLogo(row(3));
|
|
const blob = new Blob(['x'], { type: 'image/png' });
|
|
|
|
it('lädt die PNG-Datei aus dem Logo-Bucket', async () => {
|
|
mocks.fetchFile.mockResolvedValue(blob);
|
|
await expect(loadLogoBlob(logo)).resolves.toBe(blob);
|
|
expect(mocks.fetchFile).toHaveBeenCalledWith('logo-bucket', 'l3');
|
|
});
|
|
|
|
it('Fehler beim Laden gehen durch (404: Datei fehlt)', async () => {
|
|
const failure = notFound('storage_file_not_found');
|
|
mocks.fetchFile.mockRejectedValue(failure);
|
|
await expect(loadLogoBlob(logo)).rejects.toBe(failure);
|
|
});
|
|
|
|
it.each(['', '../x', 'a/b', 'a?b', '.hidden', 'x'.repeat(37)])(
|
|
'eine Datei-ID, die keine Appwrite-ID sein kann (%j): 404 „Datei fehlt“, ohne Anfrage',
|
|
async (fileId) => {
|
|
const err = await loadLogoBlob({ ...logo, fileId }).catch((e: unknown) => e);
|
|
expect(err).toBeInstanceOf(AppwriteException);
|
|
expect(err).toMatchObject({ code: 404, type: 'storage_file_not_found' });
|
|
expect(mocks.fetchFile).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|