fix(logos): hochgeladenes Logo trägt die Datei-ID der Datei und die ID des Dienstes

Die API vergibt die Zeilen-ID des Logos jetzt selbst (Hash aus Konto und Datei, nicht mehr die Datei-ID). uploadPreparedLogo baute das neue Logo
aber noch mit `fileId: id`: das Bild des frisch hochgeladenen Logos wäre aus einer Datei geladen worden, die es nicht gibt.

- upload-logo.ts: `id` aus der Antwort der API, `fileId` aus der angelegten Datei (`$id`). Kommentare ohne „Zeilen-ID = Datei-ID“ (auch logo-api.ts).
- Test: Dienst-ID und Datei-ID sind verschieden und werden nicht vertauscht (Fakes und Standard-Abhängigkeiten).
- Sonst nirgends in web/src: Auswahl und Karten-Konfiguration nehmen `logo.id`, das Bild kommt über `logo.fileId` (loadLogoBlob), beides lief schon getrennt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 05:08:39 +00:00
1 parent 6d152c11b3
commit 738358fcac
3 files changed
+24 -11

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ export interface LogoUploadInput {
name: string;
displayName: string;
mono: boolean;
/** Die Datei im Logo-Bucket (frisch hochgeladen, mit `delete("user:<ich>")`); ihre ID wird auch die ID des Logos. */
/** Die Datei im Logo-Bucket (frisch hochgeladen, mit `delete("user:<ich>")`). Das Logo bekommt vom Dienst eine eigene ID (Antwort), nicht die der Datei. */
fileId: string;
}
+18 -6
View File
@@ -36,7 +36,7 @@ const form: LogoForm = { name: 'Beispielbank', displayName: 'Kenso', mono: false
type Call = { name: string; args: unknown[] };
/** Fake-Abhängigkeiten, die jeden Aufruf in Reihenfolge aufzeichnen; die hochgeladene Datei bekommt die ID f1. */
/** Fake-Abhängigkeiten, die jeden Aufruf in Reihenfolge aufzeichnen; die hochgeladene Datei bekommt die ID f1, das Logo der Dienst die Zeilen-ID r1. */
function fakeDeps(overrides: Partial<UploadLogoDeps> = {}) {
const calls: Call[] = [];
const log = <T>(name: string, args: unknown[], result: T): T => {
@@ -47,7 +47,7 @@ function fakeDeps(overrides: Partial<UploadLogoDeps> = {}) {
userId: () => log('userId', [], 'u1'),
createFile: async (file, permissions) => log('createFile', [file, permissions], { $id: 'f1' }),
deleteFile: async (id) => log('deleteFile', [id], undefined),
uploadLogo: async (input) => log('uploadLogo', [input], { id: 'f1' }),
uploadLogo: async (input) => log('uploadLogo', [input], { id: 'r1' }),
...overrides,
};
return {
@@ -100,11 +100,11 @@ describe('uploadPreparedLogo', () => {
expect(logo.mono).toBe(sent);
});
it('Erfolg: das neue Logo (ID = Datei-ID, mein Konto, ohne Herzen und Meldungen), nichts wird gelöscht', async () => {
it('Erfolg: das neue Logo (ID des Dienstes, Datei-ID der hochgeladenen Datei, mein Konto, ohne Herzen und Meldungen), nichts wird gelöscht', async () => {
const fake = fakeDeps();
const logo = await uploadPreparedLogo(prepared(), { name: ' Beispielbank ', displayName: 'Kenso', mono: true }, fake.deps);
expect(logo).toEqual({
id: 'f1',
id: 'r1',
name: 'Beispielbank',
displayName: 'Kenso',
fileId: 'f1',
@@ -119,6 +119,16 @@ describe('uploadPreparedLogo', () => {
expect(fake.of('deleteFile')).toHaveLength(0);
});
it('die ID kommt aus der Antwort des Dienstes, die fileId aus der angelegten Datei: beide sind verschieden und werden nie vertauscht', async () => {
const fake = fakeDeps({
createFile: async () => ({ $id: 'datei-9' }),
uploadLogo: async () => ({ id: 'zeile-7' }),
});
const logo = await uploadPreparedLogo(prepared(), form, fake.deps);
expect(logo.id).toBe('zeile-7');
expect(logo.fileId).toBe('datei-9');
});
describe('Fehler der API: gelöscht wird nur bei eindeutiger Ablehnung', () => {
it.each([400, 401, 403, 404])(
'eindeutig abgelehnt (%i): die Datei dieses Versuchs wird gelöscht, der Fehler kommt an',
@@ -239,7 +249,8 @@ describe('Standard-Abhängigkeiten (Appwrite-Bucket logo-images)', () => {
// Wie Appwrite: die Datei hat die ID, mit der sie angelegt wurde.
mocks.createFile.mockImplementation(async ({ fileId }: { fileId: string }) => ({ $id: fileId }));
mocks.deleteFile.mockResolvedValue({});
mocks.uploadLogo.mockImplementation(async ({ fileId }: { fileId: string }) => ({ id: fileId }));
// Wie der Dienst: die Zeile des Logos bekommt eine eigene ID, nicht die der Datei.
mocks.uploadLogo.mockImplementation(async ({ fileId }: { fileId: string }) => ({ id: `zeile-${fileId}` }));
});
it('lädt mit frischer ID in den Logo-Bucket und ruft die API mit genau dieser ID', async () => {
@@ -256,7 +267,8 @@ describe('Standard-Abhängigkeiten (Appwrite-Bucket logo-images)', () => {
expect(file.name).toBe('logo.png');
expect(permissions).toEqual(['read("any")', 'delete("user:u1")']);
expect(mocks.uploadLogo).toHaveBeenCalledWith({ name: 'Beispielbank', displayName: 'Kenso', mono: false, fileId });
expect(logo.id).toBe(fileId);
expect(logo.id).toBe(`zeile-${fileId}`);
expect(logo.fileId).toBe(fileId);
expect(mocks.deleteFile).not.toHaveBeenCalled();
});
+5 -4
View File
@@ -58,13 +58,14 @@ function isLogoTaken(err: unknown): boolean {
/**
* Lädt das Logo hoch: das PNG in den Logo-Bucket (`read("any")`, `delete("user:<ich>")`), dann legt die API das Logo daraus an.
* Liefert das neue Logo, so wie die Liste es danach zeigt (die Zeilen-ID ist die Datei-ID). „Einfarbig“ gilt nur für Logos mit
* durchsichtigen Stellen. Name und Anzeigename werden getrimmt, ihre Grenzen prüft der Dienst.
* Liefert das neue Logo, so wie die Liste es danach zeigt: `id` ist die Zeilen-ID aus der Antwort des Dienstes (nicht die Datei-ID),
* `fileId` die ID der hochgeladenen Datei. „Einfarbig“ gilt nur für Logos mit durchsichtigen Stellen. Name und Anzeigename werden
* getrimmt, ihre Grenzen prüft der Dienst.
*
* Aufräumen (nur die eine Datei dieses Versuchs, nie etwas anderes):
* - Scheitert der Upload, wurde die API noch nicht gerufen: es gibt kein Logo, das auf die Datei zeigen könnte.
* - Scheitert die API eindeutig (4xx außer 408/429, auch das Limit von 30 Logos, oder schon der JWT-Abruf), wurde nichts
* angelegt: die Datei wird gelöscht. Ausnahme `logoTaken` (409): ein anderes Logo trägt diese Datei-ID, die Datei bleibt.
* angelegt: die Datei wird gelöscht. Ausnahme `logoTaken` (409): ein Logo trägt diese Datei schon, die Datei bleibt.
* - Bleibt der Ausgang offen (Netzfehler, Zeitlimit, 5xx, 408, 429, Antwort ohne `id`), kann die API das Logo schon angelegt
* haben. Dann bleibt die Datei liegen: sonst zeigte ein öffentliches Logo auf ein gelöschtes Bild.
* Der Fehler wird in jedem Fall weitergeworfen.
@@ -96,7 +97,7 @@ export async function uploadPreparedLogo(
id,
name,
displayName,
fileId: id,
fileId,
ownerId: userId,
mono,
likeCount: 0,