ki integration

This commit is contained in:
2026-08-15 13:43:52 +02:00
parent 82d98bd8cf
commit 9ecb292c26
78 changed files with 12055 additions and 55 deletions

119
scripts/test-mandanten.mjs Normal file
View File

@@ -0,0 +1,119 @@
/**
* 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);