diff --git a/web/src/account/cards.test.ts b/web/src/account/cards.test.ts index f943a65..dc08f9b 100644 --- a/web/src/account/cards.test.ts +++ b/web/src/account/cards.test.ts @@ -350,7 +350,7 @@ describe('save: bestehende Karte', () => { it('ohne Bildwechsel: nur neue Vorschau, Update, dann alte Vorschau löschen', async () => { const fake = fakeApi([existing()]); - const c = config({ network: 'jcb' }); + const c = config({ logo: 'abc123' }); const saved = await service(fake).save( newInput({ name: 'Neu', config: c, imageChanged: false, previous: previous() }), ); @@ -370,7 +370,7 @@ describe('save: bestehende Karte', () => { expect(saved.id).toBe('row1'); expect(saved.imageFileId).toBe('oldImage'); expect(saved.previewFileId).toBe('file1'); - expect(saved.config.network).toBe('jcb'); + expect(saved.config.logo).toBe('abc123'); }); it('veralteter Schnappschuss (Bild in anderem Tab ersetzt): das Update schickt kein altes imageFileId, die Zeile behält das neue Bild', async () => { diff --git a/web/src/account/cards.ts b/web/src/account/cards.ts index 6e32312..89b24a9 100644 --- a/web/src/account/cards.ts +++ b/web/src/account/cards.ts @@ -1,7 +1,7 @@ // Gespeicherte Karten: Dienst (Regeln fürs Speichern, Aufräumen, Laden) und Anschluss an Appwrite. // Der Dienst kennt nur das schmale `CardsApi`-Interface und lässt sich dadurch ohne Netzwerk testen. import { AppwriteException, ID, Permission, Query, Role, type Models } from 'appwrite'; -import { parseConfig, serializeConfig, type CardConfig } from '../card/config'; +import { ID_PATTERN, parseConfig, serializeConfig, type CardConfig } from '../card/config'; import type { SavedCardRef } from '../editor/state'; import { APPWRITE_ENDPOINT, IDS, client, storage, tables } from './appwrite'; import { currentUserId } from './auth'; @@ -46,8 +46,8 @@ export interface SaveInput { export const DEFAULT_CARD_NAME = 'Meine Karte'; const NAME_MAX = 80; -/** Zeilen- und Datei-IDs von Appwrite: höchstens 36 Zeichen, kein Sonderzeichen am Anfang. */ -export const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/; +/** Zeilen- und Datei-IDs von Appwrite (definiert in card/config.ts, dort braucht sie auch die Logo-ID der Konfiguration). */ +export { ID_PATTERN }; /** Trimmt, fasst Leerraum zusammen und kürzt auf 80 Zeichen (Codepoints); leer ergibt „Meine Karte“. */ export function normalizeName(name: string): string { diff --git a/web/src/assets/marks/amex-blue.svg b/web/src/assets/marks/amex-blue.svg deleted file mode 100644 index ccb37e6..0000000 --- a/web/src/assets/marks/amex-blue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/amex-outline.svg b/web/src/assets/marks/amex-outline.svg deleted file mode 100644 index a569042..0000000 --- a/web/src/assets/marks/amex-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/amex-select.svg b/web/src/assets/marks/amex-select.svg deleted file mode 100644 index ccb37e6..0000000 --- a/web/src/assets/marks/amex-select.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/apple.svg b/web/src/assets/marks/apple.svg deleted file mode 100644 index 31b057a..0000000 --- a/web/src/assets/marks/apple.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/discover.svg b/web/src/assets/marks/discover.svg deleted file mode 100644 index d55863a..0000000 --- a/web/src/assets/marks/discover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/jcb.svg b/web/src/assets/marks/jcb.svg deleted file mode 100644 index 133310f..0000000 --- a/web/src/assets/marks/jcb.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-classic.svg b/web/src/assets/marks/mc-classic.svg deleted file mode 100644 index 37a8330..0000000 --- a/web/src/assets/marks/mc-classic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-color.svg b/web/src/assets/marks/mc-color.svg deleted file mode 100644 index 1aef221..0000000 --- a/web/src/assets/marks/mc-color.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-silver-text.svg b/web/src/assets/marks/mc-silver-text.svg deleted file mode 100644 index 4aa4250..0000000 --- a/web/src/assets/marks/mc-silver-text.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-silver.svg b/web/src/assets/marks/mc-silver.svg deleted file mode 100644 index fb19c12..0000000 --- a/web/src/assets/marks/mc-silver.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/mc-wordmark.svg b/web/src/assets/marks/mc-wordmark.svg deleted file mode 100644 index c24ddcc..0000000 --- a/web/src/assets/marks/mc-wordmark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-color.svg b/web/src/assets/marks/up-color.svg deleted file mode 100644 index 8d51b91..0000000 --- a/web/src/assets/marks/up-color.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-english.svg b/web/src/assets/marks/up-english.svg deleted file mode 100644 index 8c92134..0000000 --- a/web/src/assets/marks/up-english.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-silver.svg b/web/src/assets/marks/up-silver.svg deleted file mode 100644 index 9d9dd36..0000000 --- a/web/src/assets/marks/up-silver.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/up-white-frame.svg b/web/src/assets/marks/up-white-frame.svg deleted file mode 100644 index 4660669..0000000 --- a/web/src/assets/marks/up-white-frame.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-auto.svg b/web/src/assets/marks/visa-auto.svg deleted file mode 100644 index 1f64047..0000000 --- a/web/src/assets/marks/visa-auto.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-blue.svg b/web/src/assets/marks/visa-blue.svg deleted file mode 100644 index 9fe1811..0000000 --- a/web/src/assets/marks/visa-blue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-navy.svg b/web/src/assets/marks/visa-navy.svg deleted file mode 100644 index 739c9bb..0000000 --- a/web/src/assets/marks/visa-navy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/assets/marks/visa-silver.svg b/web/src/assets/marks/visa-silver.svg deleted file mode 100644 index 673abb0..0000000 --- a/web/src/assets/marks/visa-silver.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/card/config.test.ts b/web/src/card/config.test.ts index 07ebd07..e807ae4 100644 --- a/web/src/card/config.test.ts +++ b/web/src/card/config.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { DEFAULT_CONFIG, DEFAULT_CROP, - NETWORK_IDS, + ID_PATTERN, TONE_COLOR, parseConfig, serializeConfig, @@ -12,9 +12,9 @@ import { const custom: CardConfig = { v: 1, template: 'classic', - network: 'jcb', + logo: 'abc123', details: 'dark', - brand: 'apple', + brand: 'aircards-text', crop: { x: -120.5, y: 33, zoom: 2.5 }, }; @@ -23,7 +23,7 @@ describe('Standardwerte', () => { expect(DEFAULT_CONFIG).toEqual({ v: 1, template: 'signature', - network: 'visa-auto', + logo: null, details: 'light', brand: 'aircards-symbol', crop: { x: 0, y: 0, zoom: 1 }, @@ -36,16 +36,13 @@ describe('Standardwerte', () => { expect(DEFAULT_CONFIG.crop).not.toBe(DEFAULT_CROP); }); - it('NETWORK_IDS: 18 Logos in der vereinbarten Reihenfolge', () => { - expect(NETWORK_IDS.length).toBe(18); - expect([...NETWORK_IDS]).toEqual([ - 'visa-auto', 'visa-blue', 'visa-navy', 'visa-silver', - 'mc-color', 'mc-silver', 'mc-wordmark', 'mc-silver-text', 'mc-classic', - 'up-color', 'up-silver', 'up-white-frame', 'up-english', - 'amex-blue', 'amex-outline', 'amex-select', - 'jcb', 'discover', - ]); - expect(new Set(NETWORK_IDS).size).toBe(18); + it('eine neue Karte hat kein Logo unten rechts', () => { + expect(DEFAULT_CONFIG.logo).toBeNull(); + }); + + it('ID_PATTERN: Appwrite-IDs mit höchstens 36 Zeichen und ohne Sonderzeichen am Anfang', () => { + for (const ok of ['abc123', 'A', '0', 'a.b_c-d', 'x'.repeat(36)]) expect(ID_PATTERN.test(ok), ok).toBe(true); + for (const bad of ['', 'kaputt id', '-abc', '_abc', '.abc', 'ä', 'a/b', 'x'.repeat(37)]) expect(ID_PATTERN.test(bad), bad).toBe(false); }); it('TONE_COLOR: Hell weiß, Dunkel fast schwarz', () => { @@ -72,9 +69,31 @@ describe('parseConfig / serializeConfig', () => { } }); - it('unbekanntes Netzwerk setzt nur dieses Feld auf den Standard', () => { - const raw = JSON.stringify({ ...custom, network: 'paypal' }); - expect(parseConfig(raw)).toEqual({ ...custom, network: DEFAULT_CONFIG.network }); + it('eine alte Konfiguration mit network und Apple-Logo: network entfällt, Logo oben links fällt auf den Standard', () => { + const raw = '{"v":1,"template":"classic","network":"mc-color","brand":"apple","details":"dark"}'; + expect(parseConfig(raw)).toEqual({ + v: 1, + template: 'classic', + logo: null, + details: 'dark', + brand: 'aircards-symbol', + crop: { x: 0, y: 0, zoom: 1 }, + }); + // andere unbekannte Marken ebenso + expect(parseConfig('{"brand":"visa"}').brand).toBe('aircards-symbol'); + }); + + it('logo: eine gültige ID bleibt, alles andere ergibt null', () => { + expect(parseConfig('{"logo":"abc123"}').logo).toBe('abc123'); + expect(parseConfig(JSON.stringify({ logo: 'x'.repeat(36) })).logo).toBe('x'.repeat(36)); + for (const bad of ['kaputt id', '', '-abc', 'x'.repeat(37), 42, true, null, ['abc'], { id: 'abc' }]) { + expect(parseConfig(JSON.stringify({ logo: bad })).logo, JSON.stringify(bad)).toBeNull(); + } + expect(parseConfig('{}').logo).toBeNull(); + }); + + it('das gewählte Logo bleibt nur dem Feld logo zugeordnet: ein altes network wird nie zum Logo', () => { + expect(parseConfig('{"network":"abc123"}').logo).toBeNull(); }); it('jedes Feld wird einzeln validiert', () => { @@ -88,11 +107,11 @@ describe('parseConfig / serializeConfig', () => { }); it('Namen aus Object.prototype gelten nicht als gültige Werte', () => { - const raw = JSON.stringify({ ...custom, network: 'constructor', template: '__proto__', brand: 'toString' }); + const raw = JSON.stringify({ ...custom, template: '__proto__', details: 'constructor', brand: 'toString' }); expect(parseConfig(raw)).toEqual({ ...custom, - network: DEFAULT_CONFIG.network, template: DEFAULT_CONFIG.template, + details: DEFAULT_CONFIG.details, brand: DEFAULT_CONFIG.brand, }); }); @@ -137,21 +156,29 @@ describe('parseConfig / serializeConfig', () => { const raw = JSON.stringify({ ...custom, evil: '