// Tests für web/scripts/appwrite-setup.mjs. Aufruf: node --test web/scripts/appwrite-setup.test.mjs // Nur Node-Bordmittel. Der "Appwrite-Server" ist ein Fake auf Port 0 (Speicher im Prozess), // es wird nie ein echter Server oder ein echter Key benutzt. import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { DESIRED, DEFAULT_ENDPOINT, isMissing, compareTable, compareColumn, compareBucket, main, } from './appwrite-setup.mjs'; const SCRIPT = fileURLToPath(new URL('./appwrite-setup.mjs', import.meta.url)); const KEY = 'test-key-0123456789abcdef-geheim'; // --------------------------------------------------------------------------- // Fake-Appwrite (1.8-ähnliche REST-Formen, alles im Speicher) // --------------------------------------------------------------------------- function createFakeAppwrite({ project = 'aircard', key = KEY, stuckColumns = false, inject = null } = {}) { const state = { databases: new Map(), tables: new Map(), // "db/table" columns: new Map(), // "db/table/key" buckets: new Map(), requests: [], }; const send = (res, status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); }; const fail = (res, status, type, message) => send(res, status, { message, code: status, type, version: '1.8.1' }); const server = http.createServer(async (req, res) => { const chunks = []; for await (const chunk of req) chunks.push(chunk); const raw = Buffer.concat(chunks).toString(); const body = raw ? JSON.parse(raw) : undefined; const path = new URL(req.url, 'http://fake').pathname; state.requests.push({ method: req.method, path, body, headers: req.headers }); if (req.headers['x-appwrite-project'] !== project) { return fail(res, 404, 'project_not_found', 'Project with the requested ID could not be found.'); } if (req.headers['x-appwrite-key'] !== key) { return fail(res, 401, 'user_unauthorized', 'The current user is not authorized to perform the requested action.'); } const injected = inject?.(req.method, path); if (injected) return fail(res, injected.status, injected.type, injected.message ?? 'injected'); const seg = path.replace(/^\/v1/, '').split('/').filter(Boolean); const m = req.method; // POST /tablesdb if (m === 'POST' && seg.length === 1 && seg[0] === 'tablesdb') { if (state.databases.has(body.databaseId)) return fail(res, 409, 'database_already_exists', 'exists'); const db = { $id: body.databaseId, name: body.name, enabled: true }; state.databases.set(db.$id, db); return send(res, 201, db); } // GET /tablesdb/:db if (m === 'GET' && seg.length === 2 && seg[0] === 'tablesdb') { const db = state.databases.get(seg[1]); return db ? send(res, 200, db) : fail(res, 404, 'database_not_found', 'Database not found'); } // POST /tablesdb/:db/tables if (m === 'POST' && seg.length === 3 && seg[0] === 'tablesdb' && seg[2] === 'tables') { if (!state.databases.has(seg[1])) return fail(res, 404, 'database_not_found', 'Database not found'); const id = `${seg[1]}/${body.tableId}`; if (state.tables.has(id)) return fail(res, 409, 'table_already_exists', 'exists'); const table = { $id: body.tableId, name: body.name, $permissions: body.permissions ?? [], rowSecurity: body.rowSecurity ?? true, enabled: true, }; state.tables.set(id, table); return send(res, 201, table); } // GET /tablesdb/:db/tables/:t if (m === 'GET' && seg.length === 4 && seg[0] === 'tablesdb' && seg[2] === 'tables') { if (!state.databases.has(seg[1])) return fail(res, 404, 'database_not_found', 'Database not found'); const table = state.tables.get(`${seg[1]}/${seg[3]}`); return table ? send(res, 200, table) : fail(res, 404, 'table_not_found', 'Table not found'); } // POST /tablesdb/:db/tables/:t/columns/string if (m === 'POST' && seg.length === 6 && seg[2] === 'tables' && seg[4] === 'columns' && seg[5] === 'string') { if (!state.tables.has(`${seg[1]}/${seg[3]}`)) return fail(res, 404, 'table_not_found', 'Table not found'); const id = `${seg[1]}/${seg[3]}/${body.key}`; if (state.columns.has(id)) return fail(res, 409, 'column_already_exists', 'exists'); const col = { key: body.key, type: 'string', size: body.size, required: body.required, array: false, status: 'processing', gets: 0 }; state.columns.set(id, col); const { gets, ...out } = col; return send(res, 202, out); } // GET /tablesdb/:db/tables/:t/columns/:key if (m === 'GET' && seg.length === 6 && seg[2] === 'tables' && seg[4] === 'columns') { if (!state.tables.has(`${seg[1]}/${seg[3]}`)) return fail(res, 404, 'table_not_found', 'Table not found'); const col = state.columns.get(`${seg[1]}/${seg[3]}/${seg[5]}`); if (!col) return fail(res, 404, 'column_not_found', 'Column not found'); col.gets += 1; const status = stuckColumns ? 'processing' : col.gets >= 2 ? 'available' : 'processing'; const { gets, ...out } = col; return send(res, 200, { ...out, status }); } // POST /storage/buckets if (m === 'POST' && path === '/v1/storage/buckets') { if (state.buckets.has(body.bucketId)) return fail(res, 409, 'storage_bucket_already_exists', 'exists'); const bucket = { $id: body.bucketId, name: body.name, $permissions: body.permissions ?? [], fileSecurity: body.fileSecurity, maximumFileSize: body.maximumFileSize, allowedFileExtensions: body.allowedFileExtensions, compression: body.compression, antivirus: body.antivirus, enabled: true, }; state.buckets.set(bucket.$id, bucket); return send(res, 201, bucket); } // GET /storage/buckets/:id if (m === 'GET' && seg.length === 3 && seg[0] === 'storage' && seg[1] === 'buckets') { const bucket = state.buckets.get(seg[2]); return bucket ? send(res, 200, bucket) : fail(res, 404, 'storage_bucket_not_found', 'Bucket not found'); } return fail(res, 404, 'general_route_not_found', 'Route not found'); }); return new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { const { port } = server.address(); resolve({ state, endpoint: `http://127.0.0.1:${port}/v1`, posts: () => state.requests.filter((r) => r.method === 'POST'), otherWrites: () => state.requests.filter((r) => !['GET', 'POST'].includes(r.method)), close: () => new Promise((r) => { server.closeAllConnections?.(); server.close(r); }), }); }); }); } async function withFake(opts, fn) { const fake = await createFakeAppwrite(opts); try { return await fn(fake); } finally { await fake.close(); } } async function run(fake, opts = {}) { const lines = []; const code = await main({ endpoint: fake.endpoint, key: KEY, project: 'aircard', check: false, log: (line) => lines.push(line), pollMs: 1, timeoutMs: 2000, ...opts, }); return { code, lines }; } const startsWith = (lines, prefix) => lines.filter((l) => l.startsWith(prefix)); function runCli(args, env) { return new Promise((resolve, reject) => { const child = spawn(process.execPath, [SCRIPT, ...args], { env: { PATH: process.env.PATH, ...env }, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.on('data', (d) => { stdout += d; }); child.stderr.on('data', (d) => { stderr += d; }); child.on('error', reject); child.on('close', (code) => resolve({ code, stdout, stderr })); }); } // --------------------------------------------------------------------------- // Reine Helfer // --------------------------------------------------------------------------- describe('DESIRED', () => { test('entspricht dem Schema aus der Spec', () => { assert.deepEqual(DESIRED.database, { id: 'aircard', name: 'AirCard' }); assert.equal(DESIRED.table.id, 'cards'); assert.equal(DESIRED.table.name, 'Cards'); assert.deepEqual(DESIRED.table.permissions, ['create("users")']); assert.equal(DESIRED.table.rowSecurity, true); assert.deepEqual( DESIRED.table.columns.map((c) => [c.key, c.size, c.required]), [['name', 80, true], ['config', 2000, true], ['imageFileId', 36, true], ['previewFileId', 36, true]], ); assert.deepEqual(DESIRED.bucket, { id: 'card-images', name: 'Card Images', permissions: ['create("users")'], fileSecurity: true, maximumFileSize: 5242880, allowedFileExtensions: ['png', 'jpg', 'jpeg', 'webp'], compression: 'none', antivirus: false, }); }); test('Standard-Endpoint ist der Server von WEBklar', () => { assert.equal(DEFAULT_ENDPOINT, 'https://appwrite.webklar.com/v1'); }); }); describe('isMissing', () => { test('404 mit anderem Typ gilt als fehlend', () => { assert.equal(isMissing(404, { type: 'database_not_found' }), true); assert.equal(isMissing(404, { type: 'table_not_found' }), true); assert.equal(isMissing(404, { type: 'column_not_found' }), true); assert.equal(isMissing(404, { message: 'kein JSON' }), true); assert.equal(isMissing(404, undefined), true); }); test('404 project_not_found ist NICHT "fehlt"', () => { assert.equal(isMissing(404, { type: 'project_not_found' }), false); }); test('alle anderen Status sind nicht "fehlt"', () => { assert.equal(isMissing(200, {}), false); assert.equal(isMissing(401, { type: 'user_unauthorized' }), false); assert.equal(isMissing(403, {}), false); assert.equal(isMissing(500, { type: 'general_server_error' }), false); }); }); describe('compareTable', () => { const ok = () => ({ $id: 'cards', name: 'Cards', $permissions: ['create("users")'], rowSecurity: true }); test('passende Tabelle: keine Abweichung', () => { assert.deepEqual(compareTable(ok(), DESIRED.table), []); }); test('Rechte werden unabhängig von der Reihenfolge verglichen', () => { const desired = { ...DESIRED.table, permissions: ['create("users")', 'read("any")'] }; const actual = { ...ok(), $permissions: ['read("any")', 'create("users")'] }; assert.deepEqual(compareTable(actual, desired), []); }); test('abweichende oder zusätzliche Rechte werden gemeldet', () => { const wrong = compareTable({ ...ok(), $permissions: ['read("any")'] }, DESIRED.table); assert.equal(wrong.length, 1); assert.match(wrong[0], /^\$permissions ist \[read\("any"\)\], erwartet \[create\("users"\)\]$/); const extra = compareTable({ ...ok(), $permissions: ['create("users")', 'read("any")'] }, DESIRED.table); assert.equal(extra.length, 1); assert.match(extra[0], /^\$permissions /); }); test('rowSecurity aus wird gemeldet', () => { assert.deepEqual(compareTable({ ...ok(), rowSecurity: false }, DESIRED.table), [ 'rowSecurity ist false, erwartet true', ]); }); test('mehrere Abweichungen ergeben mehrere Meldungen', () => { assert.equal(compareTable({ ...ok(), rowSecurity: false, $permissions: [] }, DESIRED.table).length, 2); }); }); describe('compareColumn', () => { const desired = DESIRED.table.columns[0]; // name, 80, required const ok = () => ({ key: 'name', type: 'string', size: 80, required: true, status: 'available' }); test('passende Spalte: keine Abweichung', () => { assert.deepEqual(compareColumn(ok(), desired), []); }); test('size, required und status werden gemeldet', () => { assert.deepEqual(compareColumn({ ...ok(), size: 100 }, desired), ['size ist 100, erwartet 80']); assert.deepEqual(compareColumn({ ...ok(), required: false }, desired), ['required ist false, erwartet true']); assert.deepEqual(compareColumn({ ...ok(), status: 'processing' }, desired), ['status ist processing, erwartet available']); }); test('fehlende Felder (z. B. falscher Spaltentyp ohne size) werden gemeldet', () => { const [msg] = compareColumn({ key: 'name', type: 'integer', required: true, status: 'available' }, desired); assert.equal(msg, 'size ist undefined, erwartet 80'); }); }); describe('compareBucket', () => { const ok = () => ({ $id: 'card-images', name: 'Card Images', $permissions: ['create("users")'], fileSecurity: true, maximumFileSize: 5242880, allowedFileExtensions: ['png', 'jpg', 'jpeg', 'webp'], compression: 'none', antivirus: false, }); test('passender Bucket: keine Abweichung', () => { assert.deepEqual(compareBucket(ok(), DESIRED.bucket), []); }); test('Endungen werden sortiert verglichen', () => { assert.deepEqual(compareBucket({ ...ok(), allowedFileExtensions: ['webp', 'jpeg', 'png', 'jpg'] }, DESIRED.bucket), []); }); test('fehlende oder zusätzliche Endungen werden gemeldet (sortiert ausgegeben)', () => { assert.deepEqual(compareBucket({ ...ok(), allowedFileExtensions: ['png', 'webp'] }, DESIRED.bucket), [ 'allowedFileExtensions ist [png, webp], erwartet [jpeg, jpg, png, webp]', ]); assert.equal(compareBucket({ ...ok(), allowedFileExtensions: [] }, DESIRED.bucket).length, 1); assert.equal(compareBucket({ ...ok(), allowedFileExtensions: ['png', 'jpg', 'jpeg', 'webp', 'gif'] }, DESIRED.bucket).length, 1); }); test('fileSecurity und maximumFileSize werden gemeldet', () => { assert.deepEqual(compareBucket({ ...ok(), fileSecurity: false }, DESIRED.bucket), ['fileSecurity ist false, erwartet true']); assert.deepEqual(compareBucket({ ...ok(), maximumFileSize: 30000000 }, DESIRED.bucket), [ 'maximumFileSize ist 30000000, erwartet 5242880', ]); }); test('Rechte werden unabhängig von der Reihenfolge verglichen und Abweichungen gemeldet', () => { const desired = { ...DESIRED.bucket, permissions: ['create("users")', 'read("any")'] }; assert.deepEqual(compareBucket({ ...ok(), $permissions: ['read("any")', 'create("users")'] }, desired), []); assert.equal(compareBucket({ ...ok(), $permissions: ['read("any")'] }, DESIRED.bucket).length, 1); }); }); // --------------------------------------------------------------------------- // Ablauf gegen den Fake-Server // --------------------------------------------------------------------------- describe('main gegen Fake-Server', () => { const EXPECTED_OBJECTS = [ 'Datenbank aircard', 'Tabelle cards', 'Spalte cards.name', 'Spalte cards.config', 'Spalte cards.imageFileId', 'Spalte cards.previewFileId', 'Bucket card-images', ]; test('--check auf leerem Projekt: Exit 1, alle 7 Objekte FEHLT, nichts angelegt', async () => { await withFake({}, async (fake) => { const { code, lines } = await run(fake, { check: true }); assert.equal(code, 1); assert.deepEqual(startsWith(lines, 'FEHLT'), EXPECTED_OBJECTS.map((o) => `FEHLT: ${o}`)); assert.equal(startsWith(lines, 'OK').length, 0); assert.equal(fake.posts().length, 0); assert.equal(fake.otherWrites().length, 0); }); }); test('Anlegen, erneut prüfen, erneut anlegen (idempotent)', async () => { await withFake({}, async (fake) => { // Anlegen const first = await run(fake); assert.equal(first.code, 0); assert.deepEqual(startsWith(first.lines, 'ANGELEGT'), EXPECTED_OBJECTS.map((o) => `ANGELEGT: ${o}`)); assert.equal(startsWith(first.lines, 'FEHLT').length, 0); assert.equal(startsWith(first.lines, 'ABWEICHUNG').length, 0); assert.ok(first.lines.includes('OK: Schema vollständig')); // Bodies und Reihenfolge exakt wie im Brief assert.deepEqual( fake.posts().map((r) => [r.path, r.body]), [ ['/v1/tablesdb', { databaseId: 'aircard', name: 'AirCard' }], ['/v1/tablesdb/aircard/tables', { tableId: 'cards', name: 'Cards', permissions: ['create("users")'], rowSecurity: true }], ['/v1/tablesdb/aircard/tables/cards/columns/string', { key: 'name', size: 80, required: true }], ['/v1/tablesdb/aircard/tables/cards/columns/string', { key: 'config', size: 2000, required: true }], ['/v1/tablesdb/aircard/tables/cards/columns/string', { key: 'imageFileId', size: 36, required: true }], ['/v1/tablesdb/aircard/tables/cards/columns/string', { key: 'previewFileId', size: 36, required: true }], ['/v1/storage/buckets', { bucketId: 'card-images', name: 'Card Images', permissions: ['create("users")'], fileSecurity: true, maximumFileSize: 5242880, allowedFileExtensions: ['png', 'jpg', 'jpeg', 'webp'], compression: 'none', antivirus: false, }], ], ); // Jede Anfrage trägt die drei Header for (const r of fake.state.requests) { assert.equal(r.headers['content-type'], 'application/json'); assert.equal(r.headers['x-appwrite-project'], 'aircard'); assert.equal(r.headers['x-appwrite-key'], KEY); } // Der Key taucht nie in der Ausgabe auf assert.ok(!first.lines.join('\n').includes(KEY)); // Alle Spalten wurden vor dem Ende ausgewartet (Status available) for (const col of fake.state.columns.values()) assert.ok(col.gets >= 2, `Spalte ${col.key} nicht bis available geprüft`); // Zweite Prüfung const postsBefore = fake.posts().length; const check = await run(fake, { check: true }); assert.equal(check.code, 0); assert.deepEqual(startsWith(check.lines, 'OK: ').filter((l) => l !== 'OK: Schema vollständig'), EXPECTED_OBJECTS.map((o) => `OK: ${o}`)); assert.ok(check.lines.includes('OK: Schema vollständig')); // Zweites Anlegen legt nichts doppelt an const second = await run(fake); assert.equal(second.code, 0); assert.equal(startsWith(second.lines, 'ANGELEGT').length, 0); assert.ok(second.lines.includes('OK: Schema vollständig')); assert.equal(fake.posts().length, postsBefore); assert.equal(fake.otherWrites().length, 0); }); }); test('Abweichungen werden gemeldet, aber nicht verändert (Exit 1)', async () => { await withFake({}, async (fake) => { assert.equal((await run(fake)).code, 0); fake.state.tables.get('aircard/cards').rowSecurity = false; fake.state.tables.get('aircard/cards').$permissions = ['read("any")']; fake.state.columns.get('aircard/cards/name').size = 100; fake.state.columns.get('aircard/cards/config').required = false; fake.state.buckets.get('card-images').maximumFileSize = 30000000; fake.state.buckets.get('card-images').allowedFileExtensions = ['png', 'gif']; fake.state.buckets.get('card-images').fileSecurity = false; const requestsBefore = fake.state.requests.length; for (const check of [true, false]) { const { code, lines } = await run(fake, { check }); assert.equal(code, 1); const dev = startsWith(lines, 'ABWEICHUNG'); assert.ok(dev.includes('ABWEICHUNG: cards.rowSecurity ist false, erwartet true'), dev.join('\n')); assert.ok(dev.includes('ABWEICHUNG: cards.name.size ist 100, erwartet 80'), dev.join('\n')); assert.ok(dev.includes('ABWEICHUNG: cards.config.required ist false, erwartet true'), dev.join('\n')); assert.ok(dev.includes('ABWEICHUNG: card-images.maximumFileSize ist 30000000, erwartet 5242880'), dev.join('\n')); assert.ok(dev.includes('ABWEICHUNG: card-images.fileSecurity ist false, erwartet true'), dev.join('\n')); assert.ok(dev.some((l) => l.startsWith('ABWEICHUNG: card-images.allowedFileExtensions ist [gif, png]')), dev.join('\n')); assert.ok(dev.some((l) => l.startsWith('ABWEICHUNG: cards.$permissions ist [read("any")]')), dev.join('\n')); assert.equal(dev.length, 7); assert.ok(!lines.includes('OK: Schema vollständig')); } // Nur lesende Anfragen seit der Manipulation assert.ok(fake.state.requests.slice(requestsBefore).every((r) => r.method === 'GET')); }); }); test('404 project_not_found bricht klar ab und meldet nichts als fehlend', async () => { await withFake({ project: 'anderes-projekt' }, async (fake) => { for (const check of [true, false]) { const { code, lines } = await run(fake, { check }); assert.equal(code, 2); assert.equal(startsWith(lines, 'FEHLT').length, 0); assert.equal(startsWith(lines, 'ANGELEGT').length, 0); const abort = startsWith(lines, 'ABBRUCH'); assert.equal(abort.length, 1); assert.match(abort[0], /Projekt/); assert.match(abort[0], /aircard/); assert.match(abort[0], /docs\/appwrite-console\.md/); assert.ok(!lines.join('\n').includes(KEY)); } assert.equal(fake.posts().length, 0); }); }); test('401 (falscher Key) bricht ab, ohne den Key auszugeben', async () => { await withFake({ key: 'ein-ganz-anderer-key' }, async (fake) => { const { code, lines } = await run(fake); assert.equal(code, 2); const abort = startsWith(lines, 'ABBRUCH'); assert.equal(abort.length, 1); assert.match(abort[0], /401/); assert.match(abort[0], /API-Key/); const all = lines.join('\n'); assert.ok(!all.includes(KEY)); assert.ok(!all.includes('ein-ganz-anderer-key')); assert.equal(fake.posts().length, 0); }); }); test('Steht der Key im Servertext, wird er in der Meldung geschwärzt', async () => { const inject = (method, path) => (method === 'GET' && path === '/v1/tablesdb/aircard' ? { status: 401, type: 'user_unauthorized', message: `Ungültiger Key ${KEY} abgelehnt` } : null); await withFake({ inject }, async (fake) => { const { code, lines } = await run(fake); assert.equal(code, 2); const all = lines.join('\n'); assert.ok(!all.includes(KEY)); assert.match(all, /Ungültiger Key \*\*\* abgelehnt/); }); }); test('5xx bricht ab', async () => { const inject = (method, path) => (method === 'GET' && path === '/v1/tablesdb/aircard' ? { status: 503, type: 'general_service_unavailable', message: 'Service temporarily unavailable' } : null); await withFake({ inject }, async (fake) => { const { code, lines } = await run(fake); assert.equal(code, 2); const abort = startsWith(lines, 'ABBRUCH'); assert.equal(abort.length, 1); assert.match(abort[0], /503/); assert.equal(startsWith(lines, 'FEHLT').length, 0); assert.equal(fake.posts().length, 0); }); }); test('Spalte wird nicht verfügbar: Abbruch nach Wartezeit', async () => { await withFake({ stuckColumns: true }, async (fake) => { const { code, lines } = await run(fake, { timeoutMs: 60 }); assert.equal(code, 2); const abort = startsWith(lines, 'ABBRUCH'); assert.equal(abort.length, 1); assert.match(abort[0], /cards\.name/); assert.match(abort[0], /nicht .*verfügbar/); // nach dem Abbruch wurde kein Bucket mehr angelegt assert.equal(fake.state.buckets.size, 0); }); }); test('Server nicht erreichbar: Abbruch mit klarer Meldung', async () => { const probe = http.createServer(); await new Promise((r) => probe.listen(0, '127.0.0.1', r)); const { port } = probe.address(); await new Promise((r) => probe.close(r)); const lines = []; const code = await main({ endpoint: `http://127.0.0.1:${port}/v1`, key: KEY, project: 'aircard', check: true, log: (l) => lines.push(l), pollMs: 1, timeoutMs: 100, }); assert.equal(code, 2); assert.match(startsWith(lines, 'ABBRUCH')[0], /nicht erreichbar/); assert.ok(!lines.join('\n').includes(KEY)); }); }); // --------------------------------------------------------------------------- // Aufruf als Kommandozeile // --------------------------------------------------------------------------- describe('CLI', () => { test('ohne AIRCARD_APPWRITE_API_KEY: deutsche Fehlermeldung, Exit 2', async () => { const { code, stdout, stderr } = await runCli([], {}); assert.equal(code, 2); assert.equal(stdout, ''); assert.match(stderr, /AIRCARD_APPWRITE_API_KEY/); assert.match(stderr, /fehlt/); }); test('unbekannte Option: Exit 2', async () => { const { code, stderr } = await runCli(['--foo'], { AIRCARD_APPWRITE_API_KEY: KEY }); assert.equal(code, 2); assert.match(stderr, /Unbekannte Option/); assert.ok(!stderr.includes(KEY)); }); test('--check gegen Fake-Server über Umgebungsvariablen: Exit 1, Key nicht in der Ausgabe', async () => { await withFake({}, async (fake) => { const { code, stdout, stderr } = await runCli(['--check'], { AIRCARD_APPWRITE_API_KEY: KEY, AIRCARD_APPWRITE_ENDPOINT: fake.endpoint, }); assert.equal(code, 1); assert.match(stdout, /FEHLT: Datenbank aircard/); assert.match(stdout, /FEHLT: Bucket card-images/); assert.ok(!(stdout + stderr).includes(KEY)); assert.equal(fake.posts().length, 0); }); }); test('Anlegen und danach --check über die Kommandozeile: Exit 0', async () => { await withFake({}, async (fake) => { const env = { AIRCARD_APPWRITE_API_KEY: KEY, AIRCARD_APPWRITE_ENDPOINT: fake.endpoint }; const apply = await runCli([], env); assert.equal(apply.code, 0, apply.stdout + apply.stderr); const check = await runCli(['--check'], env); assert.equal(check.code, 0, check.stdout + check.stderr); assert.match(check.stdout, /OK: Schema vollständig/); }); }); });