fix(api): Log-Flut begrenzen und Appwrite-Fehler strenger behandeln
- Kein Zugriffsprotokoll je Anfrage mehr (abgewiesene Anfragen an das unauthentifizierte /api/melden loggten sonst unbegrenzt); nur noch Ereignis- und 5xx-Zeilen, höchstens 120 pro Minute mit Sammelzeile - Compose: logging json-file, max-size 5m, max-file 2 - appwrite.js: 404 nur mit Typ row_not_found/storage_file_not_found als "fehlt", listRows ohne rows-Array wirft, redirect: error - Appwrite-Fehler: 502 bei 0/429/5xx, sonst 500 "Interner Fehler." - Veröffentlichen legt beide Dateien auf die sichtbaren Rechte fest (erst nach dem Anlegen der Zeile, Rollback bei Fehler) - setFilePermissions mit Promise.allSettled, gemeinsamer safeToken-Helfer - Tests: 85 (Log-Flut, Log-Budget, 404-Typen, Auth-Header, Konto-Antworten) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd1597862a
commit
cd55d4c891
8 files changed
+423
-31
No files matched your search
@@ -5,6 +5,7 @@
|
||||
|
||||
const { test, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
|
||||
const { createAppwrite, AppwriteError, queryEqual, queryLimit } = require('./appwrite.js');
|
||||
const { startFakeAppwrite } = require('./fake-appwrite.js');
|
||||
@@ -209,3 +210,123 @@ test('Netzfehler: AppwriteError (unreachable) ohne Key im Text', async () => {
|
||||
(err) => err instanceof AppwriteError && err.type === 'unreachable' && !String(err.message).includes('GEHEIMER-KEY-123'),
|
||||
);
|
||||
});
|
||||
|
||||
test('404 gilt nur mit dem erwarteten Typ als „fehlt“, jedes andere 404 ist ein Fehler', async () => {
|
||||
const { fake, appwrite } = await setup();
|
||||
const rejectsWith404 = (promise, type) =>
|
||||
assert.rejects(promise, (err) => err instanceof AppwriteError && err.status === 404 && err.type === type);
|
||||
|
||||
// erwartete Typen: fehlt
|
||||
assert.equal(await appwrite.getRow('gallery', 'x'), null);
|
||||
assert.equal(await appwrite.deleteRow('gallery', 'x'), false);
|
||||
assert.equal(await appwrite.getFile('gallery-images', 'x'), null);
|
||||
assert.equal(await appwrite.deleteFile('gallery-images', 'x'), false);
|
||||
assert.equal(await appwrite.updateFilePermissions('gallery-images', 'x', []), null);
|
||||
|
||||
// unbekannte Tabelle / Datenbank / Bucket: Fehler
|
||||
await rejectsWith404(appwrite.getRow('keine-tabelle', 'x'), 'table_not_found');
|
||||
await rejectsWith404(appwrite.deleteRow('keine-tabelle', 'x'), 'table_not_found');
|
||||
await rejectsWith404(appwrite.listRows('keine-tabelle', []), 'table_not_found');
|
||||
await rejectsWith404(appwrite.getFile('kein-bucket', 'x'), 'storage_bucket_not_found');
|
||||
await rejectsWith404(appwrite.updateFilePermissions('kein-bucket', 'x', []), 'storage_bucket_not_found');
|
||||
await rejectsWith404(appwrite.deleteFile('kein-bucket', 'x'), 'storage_bucket_not_found');
|
||||
const otherDatabase = createAppwrite({ endpoint: fake.endpoint, project: fake.project, key: fake.key, database: 'andere-db' });
|
||||
await rejectsWith404(otherDatabase.getRow('gallery', 'x'), 'database_not_found');
|
||||
|
||||
// Typ von der falschen Art (Zeile statt Datei und umgekehrt) und unbekannte Route
|
||||
fake.failNext((c) => c.method === 'GET', { status: 404, type: 'storage_file_not_found' });
|
||||
await rejectsWith404(appwrite.getRow('gallery', 'x'), 'storage_file_not_found');
|
||||
fake.failNext((c) => c.method === 'GET', { status: 404, type: 'row_not_found' });
|
||||
await rejectsWith404(appwrite.getFile('gallery-images', 'x'), 'row_not_found');
|
||||
fake.failNext((c) => c.method === 'GET', { status: 404, type: 'general_route_not_found' });
|
||||
await rejectsWith404(appwrite.getRow('gallery', 'x'), 'general_route_not_found');
|
||||
});
|
||||
|
||||
test('404 ohne JSON-Antwort (z. B. von einem Proxy) ist ein Fehler, kein „fehlt“', async () => {
|
||||
const appwrite = createAppwrite({
|
||||
endpoint: 'http://appwrite.invalid/v1',
|
||||
project: 'p',
|
||||
key: 'k',
|
||||
fetchImpl: async () => new Response('404 page not found', { status: 404 }),
|
||||
});
|
||||
for (const call of [
|
||||
() => appwrite.getRow('gallery', 'x'),
|
||||
() => appwrite.getFile('gallery-images', 'x'),
|
||||
() => appwrite.deleteRow('gallery', 'x'),
|
||||
() => appwrite.deleteFile('gallery-images', 'x'),
|
||||
() => appwrite.updateFilePermissions('gallery-images', 'x', []),
|
||||
]) {
|
||||
await assert.rejects(call, (err) => err instanceof AppwriteError && err.status === 404 && err.type === 'unknown');
|
||||
}
|
||||
});
|
||||
|
||||
test('listRows: eine Antwort ohne rows-Array wirft einen AppwriteError statt still [] zu liefern', async () => {
|
||||
for (const payload of [{ total: 0 }, { rows: 'x' }, { rows: null }, [], 'text', null]) {
|
||||
const appwrite = createAppwrite({
|
||||
endpoint: 'http://appwrite.invalid/v1',
|
||||
project: 'p',
|
||||
key: 'k',
|
||||
fetchImpl: async () => new Response(JSON.stringify(payload), { status: 200, headers: { 'Content-Type': 'application/json' } }),
|
||||
});
|
||||
await assert.rejects(
|
||||
() => appwrite.listRows('reports', []),
|
||||
(err) => err instanceof AppwriteError && err.type === 'invalid_response' && err.status === 502,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
}
|
||||
const empty = createAppwrite({
|
||||
endpoint: 'http://appwrite.invalid/v1',
|
||||
project: 'p',
|
||||
key: 'k',
|
||||
fetchImpl: async () => new Response(JSON.stringify({ total: 0, rows: [] }), { status: 200 }),
|
||||
});
|
||||
assert.deepEqual(await empty.listRows('reports', []), [], 'eine echte leere Liste bleibt eine leere Liste');
|
||||
});
|
||||
|
||||
test('Alle Appwrite-Aufrufe verwenden redirect: "error" (der Key folgt keiner Weiterleitung)', async () => {
|
||||
const seen = [];
|
||||
const appwrite = createAppwrite({
|
||||
endpoint: 'http://appwrite.invalid/v1',
|
||||
project: 'p',
|
||||
key: 'k',
|
||||
fetchImpl: async (url, init) => {
|
||||
seen.push(init.redirect);
|
||||
return new Response(JSON.stringify({ rows: [], $id: 'x' }), { status: 200 });
|
||||
},
|
||||
});
|
||||
await appwrite.getRow('gallery', 'x');
|
||||
await appwrite.listRows('gallery', []);
|
||||
await appwrite.createRow('gallery', 'x', {}, []);
|
||||
await appwrite.updateRow('gallery', 'x', {});
|
||||
await appwrite.deleteRow('gallery', 'x');
|
||||
await appwrite.getFile('gallery-images', 'x');
|
||||
await appwrite.updateFilePermissions('gallery-images', 'x', []);
|
||||
await appwrite.deleteFile('gallery-images', 'x');
|
||||
await appwrite.accountFromJwt('a.b.c');
|
||||
assert.equal(seen.length, 9);
|
||||
assert.ok(seen.every((value) => value === 'error'));
|
||||
});
|
||||
|
||||
test('Eine Weiterleitung von Appwrite wird nicht verfolgt (echter Server antwortet 302)', async () => {
|
||||
const target = http.createServer((req, res) => {
|
||||
target.hits = (target.hits || 0) + 1;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end('{}');
|
||||
});
|
||||
await new Promise((resolve) => target.listen(0, '127.0.0.1', resolve));
|
||||
const redirector = http.createServer((req, res) => {
|
||||
res.writeHead(302, { Location: `http://127.0.0.1:${target.address().port}/gesammelt` });
|
||||
res.end();
|
||||
});
|
||||
await new Promise((resolve) => redirector.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const appwrite = createAppwrite({ endpoint: `http://127.0.0.1:${redirector.address().port}/v1`, project: 'p', key: 'GEHEIMER-KEY' });
|
||||
await assert.rejects(() => appwrite.getRow('gallery', 'x'), (err) => err instanceof AppwriteError && err.type === 'unreachable');
|
||||
assert.equal(target.hits || 0, 0, 'das Ziel der Weiterleitung wurde nie angefragt');
|
||||
} finally {
|
||||
redirector.closeAllConnections();
|
||||
target.closeAllConnections();
|
||||
await new Promise((resolve) => redirector.close(resolve));
|
||||
await new Promise((resolve) => target.close(resolve));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user