fix(api): Logo-Zeilen-ID ist ein Hash aus Konto und Datei, nicht mehr die Datei-ID
Die Zeilen-ID eines gelöschten Logos steht öffentlich in der Liste und in gespeicherten Karten. War sie die Datei-ID, konnte jeder eine Datei
mit genau dieser ID anlegen, als Logo hochladen und so die alte ID neu belegen: gespeicherte Karten anderer zeigten dann sein Bild.
- logos.js: Zeilen-ID = hash32("<ownerId>|<fileId>") (logoRowId); die Antwort 201 {id} trägt sie. Dieselbe Datei desselben Kontos ergibt dieselbe ID
(409 logoTaken, bei einem Altlogo über die fileId der Zeile); alle anderen Regeln bleiben. Die Logzeilen nennen die Zeilen-ID, die Fehlerzeilen
zusätzlich die Datei-ID (sie folgt nicht mehr aus der Zeilen-ID, und die Zeile ist beim Aufräumen vielleicht schon weg).
- Altlogos (Zeilen-ID = Datei-ID, eines ist live) gehen weiter: Herz, Melden, Löschen und Sichtbarkeit adressieren nur die Zeile und lesen die Datei aus fileId.
- Tests: seedLogo legt Zeile und Datei mit verschiedenen IDs an; neu: logoRowId, die ID eines gelöschten Logos lässt sich nicht neu belegen, anderes Konto
bekommt eine verwendete Datei nicht (403), Altlogo (Herz, Meldung, Ausblenden/Einblenden, Löschen, erneutes Hochladen 409).
- README und Spec: ID-Regel. Der Client (upload-logo.ts) folgt im nächsten Commit; beide gehen zusammen live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec0b51a949
commit
6d152c11b3
4 files changed
+198
-75
No files matched your search
+161
-52
@@ -11,9 +11,10 @@ const http = require('node:http');
|
||||
const { createAppwrite } = require('./appwrite.js');
|
||||
const { startFakeAppwrite, pngHeader, webpHeader } = require('./fake-appwrite.js');
|
||||
const { createApp } = require('./app.js');
|
||||
const { createLogoRoutes, logoLikeId, logoReportId, validateLogoUpload } = require('./logos.js');
|
||||
const { createLogoRoutes, logoLikeId, logoReportId, logoRowId, validateLogoUpload } = require('./logos.js');
|
||||
const {
|
||||
MSG,
|
||||
isId,
|
||||
visibleRowPermissions,
|
||||
hiddenRowPermissions,
|
||||
likeRowPermissions,
|
||||
@@ -115,10 +116,18 @@ function logoFile(fake, id, owner = ME, { width = 200, height = 100, ...rest } =
|
||||
fake.addFile({ id, bucket: LOGO_BUCKET, owner, content: pngHeader(width, height), ...rest });
|
||||
}
|
||||
|
||||
/** Veröffentlichtes Logo: Zeile und Datei mit genau den Rechten, die die API vergibt. */
|
||||
function seedLogo(fake, { id = 'logo-1', owner = ME, hidden = false, reportCount = 0, likeCount = 0, name = 'Logo', mono = false } = {}) {
|
||||
/** Zeilen-ID des Logos, das `owner` aus der Datei `fileId` anlegt (so vergibt sie die API). */
|
||||
const rowOf = (fileId, owner = ME) => logoRowId(owner, fileId);
|
||||
/** Datei-ID, die `seedLogo` einer Logo-Zeile gibt: nie gleich der Zeilen-ID (nur Altlogos haben Zeilen-ID = Datei-ID). */
|
||||
const fileOf = (id) => `file-of-${id}`;
|
||||
|
||||
/**
|
||||
* Veröffentlichtes Logo: Zeile und Datei mit genau den Rechten, die die API vergibt. Die Datei hat die ID `fileId`
|
||||
* (Standard `fileOf(id)`); ein Altlogo mit Zeilen-ID = Datei-ID legt der Test mit `fileId: id` an.
|
||||
*/
|
||||
function seedLogo(fake, { id = 'logo-1', fileId = fileOf(id), owner = ME, hidden = false, reportCount = 0, likeCount = 0, name = 'Logo', mono = false } = {}) {
|
||||
fake.addFile({
|
||||
id,
|
||||
id: fileId,
|
||||
bucket: LOGO_BUCKET,
|
||||
owner,
|
||||
permissions: hidden ? hiddenFilePermissions(owner) : visibleFilePermissions(),
|
||||
@@ -127,7 +136,7 @@ function seedLogo(fake, { id = 'logo-1', owner = ME, hidden = false, reportCount
|
||||
fake.addRow(
|
||||
'logos',
|
||||
id,
|
||||
{ name, displayName: '', fileId: id, ownerId: owner, mono, likeCount, hidden, reportCount },
|
||||
{ name, displayName: '', fileId, ownerId: owner, mono, likeCount, hidden, reportCount },
|
||||
hidden ? hiddenRowPermissions(owner) : visibleRowPermissions(),
|
||||
);
|
||||
}
|
||||
@@ -136,7 +145,7 @@ function seedLogo(fake, { id = 'logo-1', owner = ME, hidden = false, reportCount
|
||||
function seedLogoRows(fake, owner, count) {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const id = `${owner}-l${i}`;
|
||||
fake.addRow('logos', id, { name: `L${i}`, displayName: '', fileId: id, ownerId: owner, mono: false, likeCount: 0, hidden: i % 2 === 1, reportCount: 0 }, visibleRowPermissions());
|
||||
fake.addRow('logos', id, { name: `L${i}`, displayName: '', fileId: fileOf(id), ownerId: owner, mono: false, likeCount: 0, hidden: i % 2 === 1, reportCount: 0 }, visibleRowPermissions());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,13 +162,14 @@ const nonAccountCalls = (fake) => fake.calls.filter((c) => c.path !== '/v1/accou
|
||||
const filePuts = (fake, id) => fake.calls.filter((c) => c.method === 'PUT' && c.path.endsWith(`/files/${id}`));
|
||||
|
||||
/** Die Aufrufe zu einem Logo in der Reihenfolge, in der sie ankamen (Datei abrufen, lesen, festlegen, löschen; Zeile anlegen, löschen). */
|
||||
function logoSteps(fake, id) {
|
||||
function logoSteps(fake, fileId) {
|
||||
const rowId = rowOf(fileId);
|
||||
const steps = [];
|
||||
for (const c of fake.calls) {
|
||||
if (c.path.endsWith(`/files/${id}/view`)) steps.push('Anfang lesen');
|
||||
else if (c.path.endsWith(`/files/${id}`)) steps.push({ GET: 'Datei abrufen', PUT: 'Rechte festlegen', DELETE: 'Datei löschen' }[c.method]);
|
||||
if (c.path.endsWith(`/files/${fileId}/view`)) steps.push('Anfang lesen');
|
||||
else if (c.path.endsWith(`/files/${fileId}`)) steps.push({ GET: 'Datei abrufen', PUT: 'Rechte festlegen', DELETE: 'Datei löschen' }[c.method]);
|
||||
else if (c.method === 'POST' && c.path.endsWith('/tables/logos/rows')) steps.push('Zeile anlegen');
|
||||
else if (c.method === 'DELETE' && c.path.endsWith(`/tables/logos/rows/${id}`)) steps.push('Zeile löschen');
|
||||
else if (c.method === 'DELETE' && c.path.endsWith(`/tables/logos/rows/${rowId}`)) steps.push('Zeile löschen');
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
@@ -179,6 +189,16 @@ test('logoLikeId und logoReportId: erste 32 Hex von sha256, Logo-Meldungen mit d
|
||||
assert.notEqual(logoReportId('l1', 'h1'), logoReportId('l1', 'h2'));
|
||||
});
|
||||
|
||||
test('logoRowId: erste 32 Hex von sha256("<Konto>|<Datei>"), eine gültige ID, nie die Datei-ID, je Konto und Datei verschieden', () => {
|
||||
assert.equal(logoRowId('u1', 'f1'), sha32('u1|f1'));
|
||||
assert.match(logoRowId('u1', 'f1'), /^[0-9a-f]{32}$/);
|
||||
assert.ok(isId(logoRowId('u1', 'f1')));
|
||||
assert.notEqual(logoRowId('u1', 'f1'), 'f1');
|
||||
assert.equal(logoRowId('u1', 'f1'), logoRowId('u1', 'f1'), 'dieselbe Datei desselben Kontos: dieselbe ID');
|
||||
assert.notEqual(logoRowId('u1', 'f1'), logoRowId('u2', 'f1'), 'dieselbe Datei-ID bei einem anderen Konto: andere Zeilen-ID');
|
||||
assert.notEqual(logoRowId('u1', 'f1'), logoRowId('u1', 'f2'));
|
||||
});
|
||||
|
||||
test('likeRowPermissions: genau read("user:<id>"), unzulässige Nutzer-IDs werden abgelehnt', () => {
|
||||
assert.deepEqual(likeRowPermissions('abc'), ['read("user:abc")']);
|
||||
for (const bad of ['a")', 'x","read("any', '', '../x', 5, null]) assert.throws(() => likeRowPermissions(bad));
|
||||
@@ -234,15 +254,18 @@ test('createLogoRoutes liefert die fünf Routen mit Name, json und run', () => {
|
||||
|
||||
// ---- Hochladen --------------------------------------------------------------------------------------
|
||||
|
||||
test('Logo hochladen: legt eine sichtbare Zeile an (Zeilen-ID = Datei-ID), die Datei trägt danach genau read("any")', async () => {
|
||||
test('Logo hochladen: legt eine sichtbare Zeile an (Zeilen-ID = Hash aus Konto und Datei, nicht die Datei-ID), die Datei trägt danach genau read("any")', async () => {
|
||||
const { upload, fake, logs } = await setup();
|
||||
logoFile(fake, 'logo1');
|
||||
|
||||
const res = await upload(uploadBody({ name: ' Mein Logo ', mono: true }));
|
||||
const rowId = rowOf('logo1');
|
||||
assert.equal(res.status, 201);
|
||||
assert.deepEqual(res.json, { id: 'logo1' });
|
||||
assert.deepEqual(res.json, { id: rowId });
|
||||
assert.match(rowId, /^[0-9a-f]{32}$/);
|
||||
assert.equal(fake.row('logos', 'logo1'), null, 'unter der Datei-ID gibt es keine Zeile');
|
||||
|
||||
const row = fake.row('logos', 'logo1');
|
||||
const row = fake.row('logos', rowId);
|
||||
assert.deepEqual(row.$permissions, ['read("any")']);
|
||||
assert.equal(row.name, 'Mein Logo');
|
||||
assert.equal(row.displayName, 'Kenso');
|
||||
@@ -260,7 +283,7 @@ test('Logo hochladen: legt eine sichtbare Zeile an (Zeilen-ID = Datei-ID), die D
|
||||
assert.equal(c.headers['x-appwrite-key'], fake.key);
|
||||
assert.equal(c.headers['x-appwrite-jwt'], undefined);
|
||||
}
|
||||
assert.equal(logs.filter((line) => line.endsWith('logo logo1 uploaded')).length, 1);
|
||||
assert.equal(logs.filter((line) => line.endsWith(`logo ${rowId} uploaded`)).length, 1);
|
||||
});
|
||||
|
||||
test('Logo hochladen: Anzeigename ist optional, mono fehlt = false', async () => {
|
||||
@@ -268,7 +291,7 @@ test('Logo hochladen: Anzeigename ist optional, mono fehlt = false', async () =>
|
||||
logoFile(fake, 'logo1');
|
||||
const res = await upload({ name: 'Nur Name', fileId: 'logo1' });
|
||||
assert.equal(res.status, 201);
|
||||
const row = fake.row('logos', 'logo1');
|
||||
const row = fake.row('logos', rowOf('logo1'));
|
||||
assert.equal(row.displayName, '');
|
||||
assert.equal(row.mono, false);
|
||||
});
|
||||
@@ -392,18 +415,29 @@ test('Logo hochladen: dieselbe Datei zweimal ergibt 409 logoTaken, Datei und Zei
|
||||
expectError(await upload(uploadBody({ name: 'Anderer Name' })), 409, 'logoTaken');
|
||||
assert.deepEqual(fake.file('logo1').$permissions, ['read("any")']);
|
||||
assert.equal(fake.rows('logos').length, 1);
|
||||
assert.equal(fake.row('logos', 'logo1').name, 'Mein Logo');
|
||||
assert.equal(fake.row('logos', rowOf('logo1')).name, 'Mein Logo');
|
||||
|
||||
// dieselbe ID als frisch hochgeladene Datei (z. B. nach Löschen und Neuanlegen): 409, die Rechte der Datei bleiben unangetastet
|
||||
// dieselbe Datei-ID als frisch hochgeladene Datei (z. B. nach Löschen und Neuanlegen): 409, die Rechte der Datei bleiben unangetastet
|
||||
fake.addFile({ id: 'logo1', bucket: LOGO_BUCKET, owner: ME, permissions: [...UPLOAD_PERMISSIONS, 'update("user:user-me")'], content: pngHeader(50, 50) });
|
||||
expectError(await upload(uploadBody()), 409, 'logoTaken');
|
||||
assert.deepEqual(fake.file('logo1').$permissions, [...UPLOAD_PERMISSIONS, 'update("user:user-me")']);
|
||||
assert.equal(filePuts(fake, 'logo1').length, 1, 'nur der erste, erfolgreiche Upload hat Rechte gesetzt');
|
||||
});
|
||||
|
||||
test('Logo hochladen: eine Zeile mit dieser ID gehört schon jemand anderem (liegengebliebener Anker): 409 logoTaken, Datei unangetastet', async () => {
|
||||
test('Logo hochladen: ein anderes Konto bekommt eine schon verwendete Datei nicht (403), die Zeilen-ID allein hält nicht zwei Konten auseinander', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
fake.addRow('logos', 'anker', { name: 'Alt', displayName: '', fileId: 'anker', ownerId: OTHER, mono: false, likeCount: 0, hidden: false, reportCount: 0 }, visibleRowPermissions());
|
||||
logoFile(fake, 'logo1');
|
||||
assert.equal((await upload(uploadBody())).status, 201);
|
||||
|
||||
// je Konto eine eigene Zeilen-ID: dass eine Datei nie zwei Logos trägt, sichert der Besitz (nach dem Anlegen kein delete(owner) mehr)
|
||||
expectError(await upload(uploadBody(), JWT_OTHER), 403, 'foreignFile');
|
||||
assert.equal(fake.rows('logos').length, 1);
|
||||
assert.equal(fake.row('logos', rowOf('logo1', OTHER)), null);
|
||||
});
|
||||
|
||||
test('Logo hochladen: steht unter der Zeilen-ID schon eine Zeile, die nicht in der Liste des Kontos ist (Rest): 409 logoTaken, Datei unangetastet', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
fake.addRow('logos', rowOf('anker'), { name: 'Alt', displayName: '', fileId: 'andere-datei', ownerId: OTHER, mono: false, likeCount: 0, hidden: false, reportCount: 0 }, visibleRowPermissions());
|
||||
logoFile(fake, 'anker');
|
||||
expectError(await upload(uploadBody({ fileId: 'anker' })), 409, 'logoTaken');
|
||||
assert.deepEqual(fake.file('anker').$permissions, UPLOAD_PERMISSIONS);
|
||||
@@ -411,6 +445,23 @@ test('Logo hochladen: eine Zeile mit dieser ID gehört schon jemand anderem (lie
|
||||
assert.equal(filePuts(fake, 'anker').length, 0, 'ein 409 fasst die Dateirechte nie an');
|
||||
});
|
||||
|
||||
test('Logo hochladen: die ID eines gelöschten Logos lässt sich nicht mit einer Datei gleicher ID neu belegen', async () => {
|
||||
const { upload, remove, fake } = await setup();
|
||||
logoFile(fake, 'logo1');
|
||||
const { id: victimRowId } = (await upload(uploadBody())).json;
|
||||
assert.equal((await remove(victimRowId)).status, 200);
|
||||
assert.equal(fake.row('logos', victimRowId), null);
|
||||
|
||||
// die Zeilen-ID ist öffentlich (Liste, gespeicherte Karten): jemand legt eine Datei mit genau dieser ID an und lädt sie hoch
|
||||
logoFile(fake, victimRowId, OTHER);
|
||||
const res = await upload(uploadBody({ fileId: victimRowId }), JWT_OTHER);
|
||||
assert.equal(res.status, 201);
|
||||
assert.notEqual(res.json.id, victimRowId, 'die alte Zeilen-ID bleibt unbelegt');
|
||||
assert.equal(fake.row('logos', victimRowId), null);
|
||||
assert.equal(fake.row('logos', res.json.id).fileId, victimRowId);
|
||||
assert.equal(fake.row('logos', res.json.id).ownerId, OTHER);
|
||||
});
|
||||
|
||||
test('Logo hochladen: höchstens 30 Logos je Konto (ausgeblendete zählen mit), das 31. ergibt 429 tooManyLogos', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
seedLogoRows(fake, ME, 30);
|
||||
@@ -481,7 +532,7 @@ test('Logo hochladen: scheitert auch das Zurücknehmen, bleibt die Zeile stehen,
|
||||
assert.equal(filePuts(fake, 'logo1').length, 1, 'ohne erfolgreiches Zurücknehmen bekommt die Datei delete(owner) nicht zurück');
|
||||
const lines = logs.filter((line) => line.includes('rollback failed'));
|
||||
assert.equal(lines.length, 1);
|
||||
assert.match(lines[0], /^\[aircard-api\] \S+ rollback failed for logo logo1$/);
|
||||
assert.match(lines[0], new RegExp(`^\\[aircard-api\\] \\S+ rollback failed for logo ${rowOf('logo1')} \\(file logo1\\)$`));
|
||||
assert.ok(!logs.join('\n').includes(JWT_ME));
|
||||
});
|
||||
|
||||
@@ -508,7 +559,7 @@ test('Logo hochladen: ein nach dem Festlegen anderer, aber weiterhin gültiger I
|
||||
logoFile(fake, 'logo1');
|
||||
fake.swapContentAfterPermissions('logo1', pngHeader(720, 250));
|
||||
assert.equal((await upload(uploadBody())).status, 201);
|
||||
assert.ok(fake.row('logos', 'logo1'));
|
||||
assert.ok(fake.row('logos', rowOf('logo1')));
|
||||
assert.deepEqual(fake.file('logo1').$permissions, ['read("any")']);
|
||||
});
|
||||
|
||||
@@ -530,7 +581,7 @@ test('Logo hochladen: wird die Datei zwischen Prüfung und Festlegen ausgetausch
|
||||
'Zeile löschen',
|
||||
]);
|
||||
// ein Ereignis im Log (nur ID und Code), nie „uploaded“
|
||||
assert.deepEqual(logEvents(logs, 'logo1'), ['logo logo1 removed after upload check (logoFormat)']);
|
||||
assert.deepEqual(logEvents(logs, rowOf('logo1')), [`logo ${rowOf('logo1')} removed after upload check (logoFormat)`]);
|
||||
});
|
||||
|
||||
test('Logo hochladen: nach dem Festlegen zählt der Inhalt noch einmal (kein PNG-Kopf, zu klein, zu groß): 400 logoFormat, Datei und Zeile sind weg', async () => {
|
||||
@@ -550,7 +601,7 @@ test('Logo hochladen: nach dem Festlegen zählt der Inhalt noch einmal (kein PNG
|
||||
logoFile(fake, id);
|
||||
fake.swapContentAfterPermissions(id, content);
|
||||
expectError(await upload(uploadBody({ fileId: id })), 400, 'logoFormat', label);
|
||||
assert.equal(fake.row('logos', id), null, `${label}: keine Zeile`);
|
||||
assert.equal(fake.row('logos', rowOf(id)), null, `${label}: keine Zeile`);
|
||||
assert.equal(fake.file(id), null, `${label}: Datei gelöscht`);
|
||||
}
|
||||
assert.equal(fake.rows('logos').length, 0);
|
||||
@@ -564,7 +615,7 @@ test('Logo hochladen: ist die Datei nach dem Festlegen nicht mehr lesbar, ist da
|
||||
expectError(await upload(uploadBody()), 400, 'fileMissing');
|
||||
assert.equal(fake.rows('logos').length, 0);
|
||||
assert.equal(fake.file('logo1'), null);
|
||||
assert.deepEqual(logEvents(logs, 'logo1'), ['logo logo1 removed after upload check (fileMissing)']);
|
||||
assert.deepEqual(logEvents(logs, rowOf('logo1')), [`logo ${rowOf('logo1')} removed after upload check (fileMissing)`]);
|
||||
});
|
||||
|
||||
test('Logo hochladen: Appwrite-Fehler beim zweiten Lesen ist 502, nichts bleibt veröffentlicht, die Datei bekommt delete(owner) zurück, die Wiederholung klappt', async () => {
|
||||
@@ -584,34 +635,35 @@ test('Logo hochladen: Appwrite-Fehler beim zweiten Lesen ist 502, nichts bleibt
|
||||
'Zeile löschen',
|
||||
'Rechte festlegen',
|
||||
]);
|
||||
assert.deepEqual(logEvents(logs, 'logo1'), [], 'ein Fehler ohne Urteil ist kein Ereignis');
|
||||
assert.deepEqual(logEvents(logs, rowOf('logo1')), [], 'ein Fehler ohne Urteil ist kein Ereignis');
|
||||
|
||||
assert.equal((await upload(uploadBody())).status, 201);
|
||||
assert.deepEqual(fake.file('logo1').$permissions, ['read("any")']);
|
||||
});
|
||||
|
||||
test('Logo hochladen: scheitert das Aufräumen nach einer Beanstandung, steht `rollback failed for logo <id>` im Log, die Antwort bleibt 400 logoFormat', async () => {
|
||||
test('Logo hochladen: scheitert das Aufräumen nach einer Beanstandung, steht `rollback failed for logo <id> (file <id>)` im Log, die Antwort bleibt 400 logoFormat', async () => {
|
||||
const { upload, remove, fake, logs } = await setup();
|
||||
|
||||
// die Datei lässt sich nicht löschen: die Zeile wird trotzdem entfernt (sie macht das Logo für andere sichtbar)
|
||||
// die Datei lässt sich nicht löschen: die Zeile wird trotzdem entfernt (sie macht das Logo für andere sichtbar); die Zeile ist weg,
|
||||
// darum nennt die Logzeile die Datei-ID, die sich aus der Zeilen-ID nicht ableiten lässt
|
||||
logoFile(fake, 'logo1');
|
||||
fake.swapContentAfterPermissions('logo1', pngHeader(4000, 4000));
|
||||
fake.failNext((c) => c.method === 'DELETE' && c.path.endsWith('/files/logo1'), { status: 500 });
|
||||
expectError(await upload(uploadBody()), 400, 'logoFormat');
|
||||
assert.equal(fake.row('logos', 'logo1'), null);
|
||||
assert.equal(fake.row('logos', rowOf('logo1')), null);
|
||||
assert.deepEqual(fake.file('logo1').$permissions, ['read("any")'], 'die Datei blieb liegen');
|
||||
assert.deepEqual(logEvents(logs, 'logo1'), ['rollback failed for logo logo1']);
|
||||
assert.deepEqual(logEvents(logs, rowOf('logo1')), [`rollback failed for logo ${rowOf('logo1')} (file logo1)`]);
|
||||
|
||||
// die Zeile lässt sich nicht löschen: die Datei ist weg, die Zeile bleibt als Anker und der Hochlader räumt sie über /api/logos/loeschen auf
|
||||
logoFile(fake, 'logo2');
|
||||
fake.swapContentAfterPermissions('logo2', pngHeader(4000, 4000));
|
||||
fake.failNext((c) => c.method === 'DELETE' && c.path.includes('/tables/logos/rows/logo2'), { status: 500 });
|
||||
fake.failNext((c) => c.method === 'DELETE' && c.path.includes(`/tables/logos/rows/${rowOf('logo2')}`), { status: 500 });
|
||||
expectError(await upload(uploadBody({ fileId: 'logo2' })), 400, 'logoFormat');
|
||||
assert.equal(fake.file('logo2'), null);
|
||||
assert.ok(fake.row('logos', 'logo2'));
|
||||
assert.deepEqual(logEvents(logs, 'logo2'), ['rollback failed for logo logo2']);
|
||||
assert.equal((await remove('logo2')).status, 200);
|
||||
assert.equal(fake.row('logos', 'logo2'), null);
|
||||
assert.ok(fake.row('logos', rowOf('logo2')));
|
||||
assert.deepEqual(logEvents(logs, rowOf('logo2')), [`rollback failed for logo ${rowOf('logo2')} (file logo2)`]);
|
||||
assert.equal((await remove(rowOf('logo2'))).status, 200);
|
||||
assert.equal(fake.row('logos', rowOf('logo2')), null);
|
||||
});
|
||||
|
||||
test('Logo hochladen: Validierung liefert 400 mit dem passenden Code und fasst Appwrite (außer dem Konto) nicht an', async () => {
|
||||
@@ -821,7 +873,7 @@ test('Logo melden: drei verschiedene Netze blenden das Logo aus (Zeilen- und Dat
|
||||
assert.deepEqual(res.json, { ok: true });
|
||||
assert.equal(fake.row('logos', 'logo-1').reportCount, 2);
|
||||
assert.deepEqual(fake.row('logos', 'logo-1').$permissions, ['read("any")']);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, ['read("any")']);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, ['read("any")']);
|
||||
|
||||
res = await report('logo-1', '203.0.3.9', 'sonstiges');
|
||||
assert.equal(res.status, 200);
|
||||
@@ -830,7 +882,7 @@ test('Logo melden: drei verschiedene Netze blenden das Logo aus (Zeilen- und Dat
|
||||
assert.equal(row.hidden, true);
|
||||
assert.equal(row.reportCount, 3);
|
||||
assert.deepEqual(row.$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.equal(fake.rows('logo_reports').length, 3);
|
||||
|
||||
// ausgeblendetes Logo melden: 404
|
||||
@@ -964,7 +1016,7 @@ test('Logo melden: eine doppelte Meldung heilt einen früher steckengebliebenen
|
||||
assert.deepEqual(res.json, { schonGemeldet: true });
|
||||
assert.equal(fake.row('logos', 'logo-1').hidden, true);
|
||||
assert.equal(fake.row('logos', 'logo-1').reportCount, 3);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, HIDDEN_PERMISSIONS);
|
||||
});
|
||||
|
||||
test('Logo melden: Ausfall beim Anlegen der Meldung ist 502 und zählt nicht, Ausfall beim Ausblenden endet mit 502 und der nächste Versuch blendet aus', async () => {
|
||||
@@ -977,13 +1029,13 @@ test('Logo melden: Ausfall beim Anlegen der Meldung ist 502 und zählt nicht, Au
|
||||
|
||||
await report('logo-1', '203.0.1.9');
|
||||
await report('logo-1', '203.0.2.9');
|
||||
fake.failNext((c) => c.method === 'PUT' && c.path.endsWith('/files/logo-1'), { status: 500 });
|
||||
fake.failNext((c) => c.method === 'PUT' && c.path.endsWith(`/files/${fileOf('logo-1')}`), { status: 500 });
|
||||
expectError(await report('logo-1', '203.0.3.9'), 502, 'unavailable');
|
||||
assert.equal(fake.row('logos', 'logo-1').hidden, false, 'Dateien zuerst, Zeile zuletzt: die Zeile ist noch sichtbar');
|
||||
const retry = await report('logo-1', '203.0.3.9');
|
||||
assert.equal(retry.status, 200);
|
||||
assert.equal(fake.row('logos', 'logo-1').hidden, true);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, HIDDEN_PERMISSIONS);
|
||||
});
|
||||
|
||||
// ---- Löschen ----------------------------------------------------------------------------------------
|
||||
@@ -1001,16 +1053,16 @@ test('Logo löschen durch den Hochlader: Datei, Herzen, Meldungen und Zeile sind
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(res.json, { ok: true });
|
||||
assert.equal(fake.row('logos', 'logo-1'), null);
|
||||
assert.equal(fake.file('logo-1'), null);
|
||||
assert.equal(fake.file(fileOf('logo-1')), null);
|
||||
assert.deepEqual(fake.rows('logo_likes').map((r) => r.logoId), ['logo-2']);
|
||||
assert.deepEqual(fake.rows('logo_reports').map((r) => r.logoId), ['logo-2']);
|
||||
assert.ok(fake.row('logos', 'logo-2'));
|
||||
assert.ok(fake.file('logo-2'));
|
||||
assert.ok(fake.file(fileOf('logo-2')));
|
||||
assert.equal(logs.filter((line) => line.endsWith('logo logo-1 deleted')).length, 1);
|
||||
|
||||
// Reihenfolge: Datei, Herzen, Meldungen, zuletzt die Zeile
|
||||
const order = fake.calls.filter((c) => c.method === 'DELETE').map((c) => c.path.split('/').slice(-3).join('/'));
|
||||
assert.ok(order[0].endsWith('files/logo-1'), order[0]);
|
||||
assert.ok(order[0].endsWith(`files/${fileOf('logo-1')}`), order[0]);
|
||||
assert.ok(order[order.length - 1].endsWith('logos/rows/logo-1'), order[order.length - 1]);
|
||||
});
|
||||
|
||||
@@ -1041,7 +1093,7 @@ test('Logo löschen durch Admin geht, auch bei ausgeblendetem Logo', async () =>
|
||||
const res = await remove('logo-1', JWT_ADMIN);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(fake.row('logos', 'logo-1'), null);
|
||||
assert.equal(fake.file('logo-1'), null);
|
||||
assert.equal(fake.file(fileOf('logo-1')), null);
|
||||
assert.equal(fake.rows('logo_reports').length, 0);
|
||||
assert.equal(logs.filter((line) => line.endsWith('logo logo-1 deleted by admin')).length, 1);
|
||||
});
|
||||
@@ -1052,7 +1104,7 @@ test('Logo löschen durch Fremde: 403 forbidden und nichts gelöscht', async ()
|
||||
seedLikes(fake, 'logo-1', ['u1']);
|
||||
expectError(await remove('logo-1', JWT_OTHER), 403, 'forbidden');
|
||||
assert.ok(fake.row('logos', 'logo-1'));
|
||||
assert.ok(fake.file('logo-1'));
|
||||
assert.ok(fake.file(fileOf('logo-1')));
|
||||
assert.equal(fake.rows('logo_likes').length, 1);
|
||||
assert.equal(fake.calls.filter((c) => c.method === 'DELETE').length, 0);
|
||||
});
|
||||
@@ -1074,7 +1126,7 @@ test('Logo löschen ist wiederholbar, wenn die Zeile zuletzt scheitert (die Zeil
|
||||
fake.failNext((c) => c.method === 'DELETE' && c.path.includes('/tables/logos/rows/'), { status: 500 });
|
||||
expectError(await remove('logo-1'), 502, 'unavailable');
|
||||
assert.ok(fake.row('logos', 'logo-1'), 'Zeile bleibt, bis alles andere weg ist');
|
||||
assert.equal(fake.file('logo-1'), null);
|
||||
assert.equal(fake.file(fileOf('logo-1')), null);
|
||||
assert.equal(fake.rows('logo_likes').length, 0);
|
||||
|
||||
const retry = await remove('logo-1');
|
||||
@@ -1092,7 +1144,7 @@ test('Logo-Sichtbarkeit: Nicht-Admin 403 adminOnly, ohne JWT 401, nichts ändert
|
||||
expectError(await setVisible('logo-1', false, null), 401, 'unauthorized');
|
||||
assert.deepEqual(fake.row('logos', 'logo-1').$permissions, ['read("any")']);
|
||||
assert.equal(fake.row('logos', 'logo-1').hidden, false);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, ['read("any")']);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, ['read("any")']);
|
||||
});
|
||||
|
||||
test('Admin blendet ein Logo aus: Zeilen- und Dateirechte, hidden=true, Meldungen und Herzen bleiben', async () => {
|
||||
@@ -1109,7 +1161,7 @@ test('Admin blendet ein Logo aus: Zeilen- und Dateirechte, hidden=true, Meldunge
|
||||
assert.equal(row.reportCount, 1);
|
||||
assert.equal(row.likeCount, 2);
|
||||
assert.deepEqual(row.$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, HIDDEN_PERMISSIONS);
|
||||
assert.equal(fake.rows('logo_reports').length, 1);
|
||||
assert.equal(fake.rows('logo_likes').length, 2);
|
||||
assert.equal(logs.filter((line) => line.endsWith('logo logo-1 hidden by admin')).length, 1);
|
||||
@@ -1130,7 +1182,7 @@ test('Admin blendet ein Logo ein: sichtbare Rechte, hidden=false, reportCount=0,
|
||||
assert.equal(row.reportCount, 0);
|
||||
assert.equal(row.likeCount, 2);
|
||||
assert.deepEqual(row.$permissions, ['read("any")']);
|
||||
assert.deepEqual(fake.file('logo-1').$permissions, ['read("any")']);
|
||||
assert.deepEqual(fake.file(fileOf('logo-1')).$permissions, ['read("any")']);
|
||||
assert.deepEqual(fake.rows('logo_reports').map((r) => r.logoId), ['logo-2']);
|
||||
assert.equal(fake.rows('logo_likes').length, 2);
|
||||
assert.equal(logs.filter((line) => line.endsWith('logo logo-1 made visible by admin')).length, 1);
|
||||
@@ -1152,7 +1204,7 @@ test('Logo-Sichtbarkeit: Body-Prüfung und fehlendes Logo', async () => {
|
||||
test('Logo-Sichtbarkeit: scheitert das Setzen der Dateirechte, bleibt die Zeile unverändert (Dateien zuerst, Zeile zuletzt)', async () => {
|
||||
const { setVisible, fake } = await setup();
|
||||
seedLogo(fake);
|
||||
fake.failNext((c) => c.method === 'PUT' && c.path.endsWith('/files/logo-1'), { status: 500 });
|
||||
fake.failNext((c) => c.method === 'PUT' && c.path.endsWith(`/files/${fileOf('logo-1')}`), { status: 500 });
|
||||
expectError(await setVisible('logo-1', false), 502, 'unavailable');
|
||||
assert.equal(fake.row('logos', 'logo-1').hidden, false);
|
||||
assert.deepEqual(fake.row('logos', 'logo-1').$permissions, ['read("any")']);
|
||||
@@ -1160,6 +1212,63 @@ test('Logo-Sichtbarkeit: scheitert das Setzen der Dateirechte, bleibt die Zeile
|
||||
assert.equal(fake.row('logos', 'logo-1').hidden, true);
|
||||
});
|
||||
|
||||
// ---- Altlogos: Zeilen-ID = Datei-ID (vor dem Hash-Schema angelegt) -------------------------------------------
|
||||
|
||||
test('Altlogo (Zeilen-ID = Datei-ID): Herz, Meldung, Ausblenden und Einblenden gehen weiter über die Zeilen-ID, die Datei über fileId', async () => {
|
||||
const { heart, report, setVisible, fake } = await setup();
|
||||
seedLogo(fake, { id: 'alt1', fileId: 'alt1' });
|
||||
|
||||
assert.deepEqual((await heart('alt1', true)).json, { herz: true, likeCount: 1 });
|
||||
assert.deepEqual((await heart('alt1', true, JWT_OTHER)).json, { herz: true, likeCount: 2 });
|
||||
assert.deepEqual((await heart('alt1', false)).json, { herz: false, likeCount: 1 });
|
||||
assert.equal(fake.row('logos', 'alt1').likeCount, 1);
|
||||
|
||||
assert.deepEqual((await report('alt1', '203.0.1.9')).json, { ok: true });
|
||||
assert.equal(fake.row('logos', 'alt1').reportCount, 1);
|
||||
|
||||
assert.equal((await setVisible('alt1', false)).status, 200);
|
||||
assert.equal(fake.row('logos', 'alt1').hidden, true);
|
||||
assert.deepEqual(fake.file('alt1').$permissions, HIDDEN_PERMISSIONS);
|
||||
expectError(await heart('alt1', true, JWT_ADMIN), 404, 'logoGone', 'ausgeblendet');
|
||||
assert.equal((await setVisible('alt1', true)).status, 200);
|
||||
assert.equal(fake.row('logos', 'alt1').hidden, false);
|
||||
assert.deepEqual(fake.file('alt1').$permissions, ['read("any")']);
|
||||
});
|
||||
|
||||
test('Altlogo löschen: Datei, Herzen, Meldungen und Zeile sind weg, die alte ID lässt sich danach nicht neu belegen', async () => {
|
||||
const { heart, report, remove, upload, fake } = await setup();
|
||||
seedLogo(fake, { id: 'alt1', fileId: 'alt1' });
|
||||
seedLogo(fake, { id: 'alt2', fileId: 'alt2', owner: OTHER }); // bleibt unberührt
|
||||
await heart('alt1', true);
|
||||
await report('alt1', '203.0.1.9');
|
||||
assert.equal(fake.rows('logo_likes').length, 1);
|
||||
assert.equal(fake.rows('logo_reports').length, 1);
|
||||
|
||||
assert.equal((await remove('alt1')).status, 200);
|
||||
assert.equal(fake.row('logos', 'alt1'), null);
|
||||
assert.equal(fake.file('alt1'), null);
|
||||
assert.equal(fake.rows('logo_likes').length, 0);
|
||||
assert.equal(fake.rows('logo_reports').length, 0);
|
||||
assert.ok(fake.row('logos', 'alt2'));
|
||||
assert.ok(fake.file('alt2'));
|
||||
|
||||
// jemand legt eine Datei mit der alten ID an und lädt sie hoch: es entsteht eine Zeile mit neuer ID, die alte bleibt frei
|
||||
logoFile(fake, 'alt1', OTHER);
|
||||
const res = await upload(uploadBody({ fileId: 'alt1' }), JWT_OTHER);
|
||||
assert.equal(res.status, 201);
|
||||
assert.equal(res.json.id, rowOf('alt1', OTHER));
|
||||
assert.equal(fake.row('logos', 'alt1'), null);
|
||||
});
|
||||
|
||||
test('Altlogo: dieselbe Datei noch einmal hochladen ist 409 logoTaken (die Zeile trägt sie schon), keine zweite Zeile', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
seedLogo(fake, { id: 'alt1', fileId: 'alt1' });
|
||||
expectError(await upload(uploadBody({ fileId: 'alt1' })), 409, 'logoTaken');
|
||||
assert.equal(fake.rows('logos').length, 1);
|
||||
assert.deepEqual(fake.file('alt1').$permissions, ['read("any")']);
|
||||
assert.equal(filePuts(fake, 'alt1').length, 0);
|
||||
});
|
||||
|
||||
// ---- Routen, Logs, Fehler ---------------------------------------------------------------------------
|
||||
|
||||
test('Logo-Routen: nur POST mit application/json (sonst 404 bzw. 415), Appwrite wird dabei nicht berührt', async () => {
|
||||
@@ -1215,11 +1324,11 @@ test('Logs der Logo-Endpunkte enthalten nie Key, Salz, JWT, IPs oder Namen, aber
|
||||
await report('logo-9', '203.0.9.9');
|
||||
await setVisible('logo-9', true);
|
||||
fake.failNext((c) => c.method === 'GET' && c.path.includes('/tables/logos/rows/'), { status: 500 });
|
||||
await remove('logo1');
|
||||
await remove('logo1');
|
||||
await remove(rowOf('logo1'));
|
||||
await remove(rowOf('logo1'));
|
||||
|
||||
assert.ok(logs.length > 0);
|
||||
assert.ok(logs.some((line) => line.endsWith('logo logo2 removed after upload check (logoFormat)')));
|
||||
assert.ok(logs.some((line) => line.endsWith(`logo ${rowOf('logo2')} removed after upload check (logoFormat)`)));
|
||||
const all = logs.join('\n');
|
||||
for (const secret of [fake.key, SALT, JWT_ME, JWT_OTHER, JWT_ADMIN, 'xxx.yyy.zzz', '203.0.', '127.0.0.1', reporterHash(SALT, '203.0.7.9'), 'GEHEIMER']) {
|
||||
assert.ok(!all.includes(secret), `Log enthält ${secret.slice(0, 12)}…`);
|
||||
|
||||
Reference in new issue
Block a user