/** * E2-Abnahme: Mandantentrennung – geprüft über die API, nicht über das UI. * * Legt zwei echte Konten an und baut für beide Team + brands-Zeile genau so wie * client/src/lib/auth.ts. Danach: sieht Konto B irgendetwas von Konto A? * * Kein SDK, nur fetch mit eigenem Cookie-Speicher – so verhält sich der Aufruf * wie ein Browser, und der Test hängt nicht an SDK-Eigenheiten. */ const ENDPOINT = 'https://appwrite.webklar.com/v1'; const PROJECT = '6a5cee34002bb8360c34'; const DB = 'brandloop'; const KEY = process.env.APPWRITE_API_KEY; if (!KEY) { console.error('APPWRITE_API_KEY fehlt'); process.exit(1); } const stamp = Date.now(); let fehler = 0; const ok = (m) => console.log(` ok ${m}`); const bad = (m) => { fehler++; console.log(` FEHL ${m}`); }; const limit = encodeURIComponent(JSON.stringify({ method: 'limit', values: [100] })); /** Ein Konto = ein Cookie-Speicher, so wie ein Browser-Profil. */ function neueSitzung() { const jar = new Map(); return async function call(method, path, body) { const headers = { 'X-Appwrite-Project': PROJECT, 'Content-Type': 'application/json' }; if (jar.size) headers.Cookie = [...jar].map(([k, v]) => `${k}=${v}`).join('; '); const res = await fetch(`${ENDPOINT}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, redirect: 'manual', }); for (const c of res.headers.getSetCookie?.() ?? []) { const [pair] = c.split(';'); const i = pair.indexOf('='); if (i > 0) jar.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim()); } const text = await res.text(); let json; try { json = JSON.parse(text); } catch { json = { message: text }; } return { status: res.status, json }; }; } const admin = async (method, path, body) => { const res = await fetch(`${ENDPOINT}${path}`, { method, headers: { 'X-Appwrite-Project': PROJECT, 'X-Appwrite-Key': KEY, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); const text = await res.text(); let json; try { json = JSON.parse(text); } catch { json = { message: text }; } return { status: res.status, json }; }; function muss(r, was) { if (r.status >= 200 && r.status < 300) return r.json; throw new Error(`${was}: HTTP ${r.status} – ${r.json?.message}`); } /** Spiegelt client/src/lib/auth.ts: Konto → Session → Team → brands-Zeile. */ async function kontoAnlegen(kennung) { const call = neueSitzung(); const email = `e2-${kennung}-${stamp}@brandloop.test`; const password = `Test-${stamp}-${kennung}!`; const label = `Marke ${kennung.toUpperCase()}`; const user = muss(await call('POST', '/account', { userId: 'unique()', email, password, name: label }), 'account'); muss(await call('POST', '/account/sessions/email', { email, password }), 'session'); const team = muss(await call('POST', '/teams', { teamId: 'unique()', name: label }), 'team'); const rolle = `team:${team.$id}`; const brand = muss(await call('POST', `/tablesdb/${DB}/tables/brands/rows`, { rowId: 'unique()', data: { team_id: team.$id, label_name: label, status: 'trial', plan: 'trial' }, permissions: [`read("${rolle}")`, `update("${rolle}")`, `delete("${rolle}")`], }), 'brands-Zeile'); return { call, userId: user.$id, teamId: team.$id, brandId: brand.$id, label }; } console.log('Konten anlegen (wie der Client es tut):'); const A = await kontoAnlegen('a'); console.log(` A: user=${A.userId} team=${A.teamId} brand=${A.brandId}`); const B = await kontoAnlegen('b'); console.log(` B: user=${B.userId} team=${B.teamId} brand=${B.brandId}`); console.log('\nMandantentrennung:'); const liste = await B.call('GET', `/tablesdb/${DB}/tables/brands/rows?queries[]=${limit}`); const ids = (liste.json.rows ?? []).map((r) => r.$id); if (ids.includes(A.brandId)) bad(`B sieht A's Zeile in der Liste`); else ok(`B listet ${ids.length} brands-Zeile(n), A's ist nicht dabei`); if (!ids.includes(B.brandId)) bad('B sieht die EIGENE Zeile nicht – Rechte zu streng'); else ok('B sieht die eigene Zeile'); const direkt = await B.call('GET', `/tablesdb/${DB}/tables/brands/rows/${A.brandId}`); if (direkt.status < 300) bad(`B konnte A's Zeile direkt lesen (HTTP ${direkt.status})`); else ok(`Direktzugriff B→A abgewiesen (HTTP ${direkt.status} ${direkt.json?.type ?? ''})`); const schreib = await B.call('PATCH', `/tablesdb/${DB}/tables/brands/rows/${A.brandId}`, { data: { label_name: 'gekapert' } }); if (schreib.status < 300) bad(`B konnte A's Zeile ÄNDERN (HTTP ${schreib.status})`); else ok(`Schreibzugriff B→A abgewiesen (HTTP ${schreib.status} ${schreib.json?.type ?? ''})`); // Gegenprobe: sieht der Server-Key beide? Sonst wäre der erste Test wertlos, // weil dann vielleicht schlicht nichts in der Tabelle steht. const alle = await admin('GET', `/tablesdb/${DB}/tables/brands/rows?queries[]=${limit}`); const alleIds = (alle.json.rows ?? []).map((r) => r.$id); if (alleIds.includes(A.brandId) && alleIds.includes(B.brandId)) ok(`Server-Key sieht beide Zeilen (${alle.json.total} gesamt) – der erste Test ist damit aussagekräftig`); else bad(`Server-Key sieht nicht beide: ${JSON.stringify(alleIds)}`); console.log('\nAufräumen:'); for (const k of [A, B]) { await admin('DELETE', `/tablesdb/${DB}/tables/brands/rows/${k.brandId}`); await admin('DELETE', `/teams/${k.teamId}`); await admin('DELETE', `/users/${k.userId}`); } const rest = await admin('GET', `/tablesdb/${DB}/tables/brands/rows?queries[]=${limit}`); console.log(` Testkonten entfernt, brands enthält jetzt ${rest.json.total} Zeile(n).`); console.log(fehler ? `\n${fehler} FEHLER – Mandantentrennung nicht dicht.` : '\nMandantentrennung hält.'); process.exit(fehler ? 1 : 0);