'use strict'; // Tests für deploy/api/profiles.js (Benutzernamen): echte HTTP-Anfragen auf Port 0 gegen createApp, dahinter ein Fake-Appwrite im // Speicher (fake-appwrite.js, Tabelle `profiles` mit Unique-Index auf `usernameKey`) und das echte appwrite.js. // Aufruf: node --test "deploy/api/*.test.js" const { test, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const http = require('node:http'); const { createAppwrite, AppwriteError } = require('./appwrite.js'); const { startFakeAppwrite } = require('./fake-appwrite.js'); const { createApp } = require('./app.js'); const { createProfileRoutes, hasProfile } = require('./profiles.js'); const { MSG, USERNAME_PATTERN, RESERVED_USERNAMES, usernameKey, isReservedUsername } = require('./common.js'); const SALT = 'test-salt-0123456789abcdef0123456789abcdef'; const ME = 'user-me'; const OTHER = 'user-other'; const JWT_ME = 'eyJhbGciOiJI.payload-me.signature-me'; const JWT_OTHER = 'eyJhbGciOiJI.payload-other.signature-other'; /** Die Sperrliste der Spec (§ 2), wörtlich: der Test hält sie fest, damit eine Änderung auffällt. */ const SPEC_RESERVED = [ 'admin', 'administrator', 'root', 'system', 'support', 'moderator', 'mod', 'aircard', 'aircardios', 'aircard-ios', 'aircard.ios', 'aircard_ios', 'webklar', 'anonym', 'anonymous', 'null', 'undefined', 'team', 'staff', 'official', 'offiziell', 'hilfe', 'help', 'kontakt', 'contact', 'info', ]; // ---- Aufbau --------------------------------------------------------------------------------- const cleanups = []; afterEach(async () => { for (const fn of cleanups.splice(0).reverse()) await fn(); }); async function setup() { const fake = await startFakeAppwrite(); fake.addUser(JWT_ME, { id: ME }); fake.addUser(JWT_OTHER, { id: OTHER }); const logs = []; const appwrite = createAppwrite({ endpoint: fake.endpoint, project: fake.project, key: fake.key }); const app = createApp({ appwrite, salt: SALT, log: (line) => logs.push(String(line)) }); const server = http.createServer(app); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const { port } = server.address(); cleanups.push(async () => { server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); await fake.close(); }); const call = (method, path, options) => request(port, method, path, options); const post = (path, body, jwt) => call('POST', path, { body, headers: jwt ? { 'X-Aircard-Jwt': jwt } : {} }); /** POST /api/profil mit dem Namen (Standard: als ME). */ const profile = (username, jwt = JWT_ME) => post('/api/profil', { username }, jwt); return { fake, appwrite, logs, call, post, profile }; } function request(port, method, path, { headers = {}, body, raw, contentType = 'application/json' } = {}) { return new Promise((resolve, reject) => { const payload = raw !== undefined ? raw : body === undefined ? undefined : JSON.stringify(body); const requestHeaders = { ...headers }; if (payload !== undefined && contentType) requestHeaders['Content-Type'] = contentType; if (payload !== undefined) requestHeaders['Content-Length'] = Buffer.byteLength(payload); const req = http.request({ host: '127.0.0.1', port, method, path, headers: requestHeaders }, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); let json = null; try { json = JSON.parse(text); } catch { // kein JSON } resolve({ status: res.statusCode, headers: res.headers, text, json }); }); }); req.on('error', reject); if (payload !== undefined) req.write(payload); req.end(); }); } /** Erwartet Status und genau {error: , code: }. */ function expectError(res, status, code, label = code) { assert.equal(res.status, status, `${label}: Status`); assert.deepEqual(res.json, { error: MSG[code], code }, label); } const nonAccountCalls = (fake) => fake.calls.filter((c) => c.path !== '/v1/account'); /** Aufrufe bei Appwrite auf `profiles`: Zeile anlegen, per Abfrage suchen (die Vorabprüfung), Zeile ändern. */ const isCreate = (c) => c.method === 'POST' && c.path.endsWith('/tables/profiles/rows'); const isList = (c) => c.method === 'GET' && c.path.endsWith('/tables/profiles/rows'); const isUpdate = (c) => c.method === 'PATCH' && c.path.includes('/tables/profiles/rows/'); /** Wartet (höchstens etwa 2 s), bis `condition()` stimmt: so fängt ein Test den Moment ab, in dem ein Aufruf bei Appwrite noch läuft. */ async function until(condition) { for (let i = 0; i < 400 && !condition(); i += 1) await new Promise((resolve) => setTimeout(resolve, 5)); assert.ok(condition(), 'der erwartete Zustand wurde nicht erreicht'); } /** * Zwei Anfragen, die beide vorab geprüft haben („Name frei“), bevor eine davon schreibt: das Schreiben (`isWrite`) wird festgehalten, bis * beide Abfragen bei Appwrite angekommen sind. So entscheidet erst der Unique-Index, nicht die Vorabprüfung. */ async function raceTwo(fake, isWrite, first, second) { const releases = [fake.holdNext(isWrite), fake.holdNext(isWrite)]; const pending = Promise.all([first(), second()]); await until(() => fake.calls.filter(isList).length === 2); for (const release of releases) release(); return pending; } // ---- Reine Helfer (common.js) ----------------------------------------------------------------------- test('USERNAME_PATTERN: 3–20 Zeichen, erstes ein Buchstabe oder eine Zahl, danach Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich', () => { assert.equal(USERNAME_PATTERN.source, '^[A-Za-z0-9][A-Za-z0-9._-]{2,19}$'); for (const ok of ['abc', 'Kenso', 'k3nso', '007', 'a.b', 'a_b', 'a-b', 'A.b_c-d9', 'a'.repeat(20), 'Z9.']) { assert.ok(USERNAME_PATTERN.test(ok), ok); } const bad = ['', 'a', 'ab', '_abc', '.abc', '-abc', 'a'.repeat(21), 'a b', ' abc', 'abc ', 'abc\n', 'a\nbc', 'Jürgen', 'abc', 'a@b', 'a/b', 'a+b', 'abc😀']; for (const name of bad) assert.ok(!USERNAME_PATTERN.test(name), JSON.stringify(name)); }); test('usernameKey: der Name klein geschrieben, sonst unverändert', () => { assert.equal(usernameKey('Kenso'), 'kenso'); assert.equal(usernameKey('AirCard-IOS'), 'aircard-ios'); assert.equal(usernameKey('a.B_c-D9'), 'a.b_c-d9'); assert.equal(usernameKey('kenso'), 'kenso'); }); test('Sperrliste: genau die Schlüssel der Spec (§ 2), gesperrt in jeder Schreibweise; andere Namen sind frei', () => { assert.deepEqual([...RESERVED_USERNAMES].sort(), [...SPEC_RESERVED].sort()); for (const key of SPEC_RESERVED) { assert.ok(isReservedUsername(key), key); assert.ok(isReservedUsername(key.toUpperCase()), key.toUpperCase()); assert.ok(isReservedUsername(`${key[0].toUpperCase()}${key.slice(1)}`), `${key} mit großem Anfang`); // jeder gesperrte Name passt zum Muster (sonst wäre der Eintrag wirkungslos: er scheiterte schon als „ungültig“) assert.match(key, USERNAME_PATTERN); } for (const free of ['kenso', 'admins', 'admin1', 'root_', 'air-card-ios', 'aircard-ios2', 'team.x', 'xinfo', 'helpme']) { assert.ok(!isReservedUsername(free), free); } }); test('MSG: die vier Benutzernamen-Texte stehen wörtlich fest (Spec § 4)', () => { assert.equal( MSG.usernameInvalid, 'Der Benutzername muss 3–20 Zeichen lang sein, mit einem Buchstaben oder einer Zahl beginnen und darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten.', ); assert.equal(MSG.usernameReserved, 'Dieser Benutzername ist nicht erlaubt.'); assert.equal(MSG.usernameTaken, 'Dieser Benutzername ist schon vergeben.'); assert.equal(MSG.usernameRequired, 'Bitte wähle zuerst einen Benutzernamen.'); }); test('createProfileRoutes liefert genau die Route POST /api/profil mit Name, json und run', () => { const routes = createProfileRoutes({ appwrite: {}, say: () => {}, withLock: (key, fn) => fn(), requireUser: async () => ({ id: ME, isAdmin: false }), }); assert.deepEqual( routes.map(([key]) => key), ['POST /api/profil'], ); const [[, route]] = routes; assert.equal(route.name, 'profil'); assert.equal(route.json, true); assert.equal(typeof route.run, 'function'); }); test('hasProfile: true mit Zeile (Zeilen-ID = Konto-ID), false ohne; ein Appwrite-Fehler wird nicht verschluckt', async () => { const { fake, appwrite } = await setup(); assert.equal(await hasProfile(appwrite, ME), false); fake.addProfile(ME, 'Kenso'); assert.equal(await hasProfile(appwrite, ME), true); assert.equal(await hasProfile(appwrite, OTHER), false, 'ein anderes Konto hat nichts davon'); fake.failNext((c) => c.method === 'GET' && c.path.endsWith('/tables/profiles/rows/user-other'), { status: 500 }); await assert.rejects(hasProfile(appwrite, OTHER), (err) => err instanceof AppwriteError && err.status === 500); // fehlt die Tabelle ganz (Schema nicht eingespielt), ist das ein Fehler und kein „kein Profil“ fake.failNext((c) => c.method === 'GET' && c.path.endsWith('/tables/profiles/rows/user-other'), { status: 404, type: 'table_not_found' }); await assert.rejects(hasProfile(appwrite, OTHER), (err) => err instanceof AppwriteError && err.status === 404); }); test('Fake-Appwrite: der Unique-Index auf usernameKey weist eine zweite Zeile (Anlegen und Ändern) mit 409 ab', async () => { const { fake, appwrite } = await setup(); const isConflict = (err) => err instanceof AppwriteError && err.status === 409; await appwrite.createRow('profiles', 'a', { username: 'Kenso', usernameKey: 'kenso' }, ['read("any")']); await assert.rejects(appwrite.createRow('profiles', 'b', { username: 'KENSO', usernameKey: 'kenso' }, ['read("any")']), isConflict); assert.equal(fake.row('profiles', 'b'), null); await appwrite.createRow('profiles', 'b', { username: 'Other', usernameKey: 'other' }, ['read("any")']); await assert.rejects(appwrite.updateRow('profiles', 'b', { username: 'Kenso', usernameKey: 'kenso' }), isConflict); assert.equal(fake.row('profiles', 'b').usernameKey, 'other'); // die eigene Zeile ändern (nur die Schreibweise) ist kein Verstoß await appwrite.updateRow('profiles', 'a', { username: 'KENSO', usernameKey: 'kenso' }); assert.equal(fake.row('profiles', 'a').username, 'KENSO'); // Spaltenlängen wie in der echten Tabelle (username und usernameKey: 20) await assert.rejects(appwrite.createRow('profiles', 'c', { username: 'x'.repeat(21), usernameKey: 'x' }, []), (err) => err.status === 400); }); // ---- Anlegen und Ändern ---------------------------------------------------------------------------- test('Profil anlegen: 200 {username}, Zeilen-ID = Konto-ID, Rechte genau read("any"), usernameKey klein, der Name wie eingegeben', async () => { const { profile, fake } = await setup(); const res = await profile('Kenso.IOS'); assert.equal(res.status, 200); assert.deepEqual(res.json, { username: 'Kenso.IOS' }); assert.equal(fake.rows('profiles').length, 1); const row = fake.row('profiles', ME); assert.equal(row.username, 'Kenso.IOS'); assert.equal(row.usernameKey, 'kenso.ios'); assert.deepEqual(row.$permissions, ['read("any")']); // der Server-Key hängt an jedem Datenaufruf, das JWT nur am Konto-Aufruf assert.equal(fake.calls.find((c) => c.path === '/v1/account').headers['x-appwrite-jwt'], JWT_ME); for (const c of nonAccountCalls(fake)) { assert.equal(c.headers['x-appwrite-key'], fake.key); assert.equal(c.headers['x-appwrite-jwt'], undefined); } }); test('Profil anlegen: die Zeile gehört dem Konto aus dem JWT, andere Felder im Body (Konto-ID, Schlüssel, Rechte) werden ignoriert', async () => { const { post, fake } = await setup(); const res = await post( '/api/profil', { username: 'Kenso', usernameKey: 'anders', userId: OTHER, ownerId: OTHER, $id: OTHER, $permissions: ['read("any")', 'update("any")'] }, JWT_ME, ); assert.deepEqual(res.json, { username: 'Kenso' }); assert.equal(fake.row('profiles', OTHER), null); const row = fake.row('profiles', ME); assert.equal(row.usernameKey, 'kenso'); assert.deepEqual(row.$permissions, ['read("any")']); assert.deepEqual(Object.keys(row).filter((key) => !key.startsWith('$')).sort(), ['username', 'usernameKey']); }); test('Profil anlegen an den Grenzen: 3 und 20 Zeichen gehen', async () => { const { profile, fake } = await setup(); assert.deepEqual((await profile('abc')).json, { username: 'abc' }); assert.deepEqual((await profile('X'.repeat(20), JWT_OTHER)).json, { username: 'X'.repeat(20) }); assert.equal(fake.row('profiles', OTHER).usernameKey, 'x'.repeat(20)); }); test('Profil ändern: dieselbe Zeile wird aktualisiert, der alte Schlüssel ist sofort für ein anderes Konto frei, der neue vergeben', async () => { const { profile, fake } = await setup(); assert.equal((await profile('Kenso')).status, 200); expectError(await profile('kenso', JWT_OTHER), 409, 'usernameTaken', 'solange der Name vergeben ist'); const res = await profile('Kenso.neu'); assert.equal(res.status, 200); assert.deepEqual(res.json, { username: 'Kenso.neu' }); assert.equal(fake.rows('profiles').length, 1, 'eine Zeile je Konto'); assert.equal(fake.row('profiles', ME).username, 'Kenso.neu'); assert.equal(fake.row('profiles', ME).usernameKey, 'kenso.neu'); assert.deepEqual(fake.row('profiles', ME).$permissions, ['read("any")']); assert.equal(fake.calls.filter(isCreate).length, 1, 'beim Ändern wird nichts neu angelegt'); assert.equal(fake.calls.filter(isUpdate).length, 1); // der alte Name ist frei: ein anderes Konto nimmt ihn, das alte Konto bekommt ihn nicht mehr zurück assert.deepEqual((await profile('kenso', JWT_OTHER)).json, { username: 'kenso' }); expectError(await profile('Kenso'), 409, 'usernameTaken', 'zurück'); assert.equal(fake.row('profiles', ME).username, 'Kenso.neu', 'nach dem Fehlschlag bleibt das Profil, wie es war'); assert.equal(fake.row('profiles', OTHER).usernameKey, 'kenso'); }); test('„kenso“ → „Kenso“ beim selben Konto ist erlaubt (nur die Schreibweise), ebenso derselbe Name nochmal', async () => { const { profile, fake } = await setup(); assert.deepEqual((await profile('kenso')).json, { username: 'kenso' }); assert.deepEqual((await profile('Kenso')).json, { username: 'Kenso' }); assert.equal(fake.row('profiles', ME).username, 'Kenso'); assert.equal(fake.row('profiles', ME).usernameKey, 'kenso'); assert.deepEqual((await profile('Kenso')).json, { username: 'Kenso' }, 'derselbe Name nochmal'); assert.deepEqual((await profile('KENSO')).json, { username: 'KENSO' }); assert.equal(fake.rows('profiles').length, 1); assert.equal(fake.row('profiles', ME).username, 'KENSO'); }); test('vergebener Name (in jeder Schreibweise): 409 usernameTaken, es wird nichts angelegt oder geändert', async () => { const { profile, fake } = await setup(); fake.addProfile(OTHER, 'Kenso'); for (const name of ['kenso', 'KENSO', 'Kenso', 'kEnSo']) expectError(await profile(name), 409, 'usernameTaken', name); assert.equal(fake.row('profiles', ME), null, 'kein Profil angelegt'); assert.equal(fake.rows('profiles').length, 1); // wer schon ein Profil hat, behält es fake.addProfile(ME, 'Me.me'); expectError(await profile('kenso'), 409, 'usernameTaken'); assert.equal(fake.row('profiles', ME).username, 'Me.me'); assert.equal(fake.row('profiles', OTHER).username, 'Kenso', 'das andere Profil ist unberührt'); }); // ---- Gleichzeitigkeit -------------------------------------------------------------------------------- test('Rennen: zwei Konten wollen gleichzeitig denselben Namen – genau eines bekommt ihn, das andere 409 (Unique-Index, nicht nur die Vorabprüfung)', async () => { const { profile, fake } = await setup(); const results = await raceTwo( fake, isCreate, () => profile('Rennen', JWT_ME), () => profile('rennen', JWT_OTHER), ); assert.deepEqual(results.map((res) => res.status).sort(), [200, 409]); const winner = results.findIndex((res) => res.status === 200); const loser = results.find((res) => res.status === 409); assert.deepEqual(loser.json, { error: MSG.usernameTaken, code: 'usernameTaken' }); assert.equal(fake.rows('profiles').length, 1, 'genau eine Zeile'); const [row] = fake.rows('profiles'); assert.equal(row.$id, winner === 0 ? ME : OTHER); assert.equal(row.usernameKey, 'rennen'); assert.equal(results[winner].json.username, row.username); // beide Vorabprüfungen haben den Namen für frei gehalten und beide haben angelegt: erst der Unique-Index hat entschieden assert.equal(fake.calls.filter(isCreate).length, 2); }); test('Rennen beim Umbenennen: zwei Konten wollen denselben neuen Namen – genau eines bekommt ihn, das andere behält seinen alten', async () => { const { profile, fake } = await setup(); fake.addProfile(ME, 'Alt.eins'); fake.addProfile(OTHER, 'Alt.zwei'); const results = await raceTwo( fake, isUpdate, () => profile('Gemeinsam', JWT_ME), () => profile('GEMEINSAM', JWT_OTHER), ); assert.deepEqual(results.map((res) => res.status).sort(), [200, 409]); assert.deepEqual(results.find((res) => res.status === 409).json, { error: MSG.usernameTaken, code: 'usernameTaken' }); assert.equal(fake.calls.filter(isUpdate).length, 2, 'beide haben ändern wollen: erst der Unique-Index hat entschieden'); const [winner, loser] = results[0].status === 200 ? [ME, OTHER] : [OTHER, ME]; assert.equal(fake.row('profiles', winner).usernameKey, 'gemeinsam'); assert.equal(fake.row('profiles', loser).username, loser === ME ? 'Alt.eins' : 'Alt.zwei', 'wer verliert, behält den alten Namen'); assert.equal(fake.rows('profiles').length, 2); }); test('Sperre je Konto (profile:): zwei gleichzeitige Anfragen desselben Kontos laufen nacheinander, beide gelingen, am Ende gilt die zweite', async () => { const { profile, fake } = await setup(); const release = fake.holdNext(isCreate); const first = profile('Alpha'); await until(() => fake.calls.some(isCreate)); // die erste steht beim Anlegen const second = profile('Beta'); await until(() => fake.calls.filter((c) => c.path === '/v1/account').length === 2); // die zweite ist angemeldet ... await new Promise((resolve) => setTimeout(resolve, 50)); assert.equal(fake.calls.filter(isList).length, 1, '... wartet aber auf die Sperre, statt schon abzufragen'); release(); assert.deepEqual((await first).json, { username: 'Alpha' }); assert.deepEqual((await second).json, { username: 'Beta' }); assert.equal(fake.rows('profiles').length, 1); assert.equal(fake.row('profiles', ME).username, 'Beta'); assert.equal(fake.row('profiles', ME).usernameKey, 'beta'); assert.equal(fake.calls.filter(isCreate).length, 1, 'die zweite hat die Zeile der ersten geändert, nicht noch einmal angelegt'); assert.equal(fake.calls.filter(isUpdate).length, 1); }); // ---- Prüfungen ----------------------------------------------------------------------------------------- test('gesperrte Namen (Spec § 2, in jeder Schreibweise): 400 usernameReserved, ohne Abfrage bei Appwrite', async () => { const { profile, fake } = await setup(); for (const name of ['Admin', 'AirCard-IOS', 'AIRCARD.IOS', 'Aircard_Ios', 'ROOT', 'Null', ...SPEC_RESERVED]) { expectError(await profile(name), 400, 'usernameReserved', name); } assert.equal(fake.rows('profiles').length, 0); assert.equal(nonAccountCalls(fake).length, 0, 'die Prüfung braucht Appwrite nur für das JWT'); }); test('ungültige Namen (Muster): 400 usernameInvalid, nichts angelegt, ohne Abfrage bei Appwrite', async () => { const { profile, fake } = await setup(); const cases = [ ['zu kurz', 'ab'], ['ein Zeichen', 'a'], ['leer', ''], ['Unterstrich vorn', '_abc'], ['Punkt vorn', '.abc'], ['Bindestrich vorn', '-abc'], ['21 Zeichen', 'a'.repeat(21)], ['Leerzeichen', 'a b'], ['Leerzeichen vorn', ' abc'], ['Leerzeichen hinten', 'abc '], ['Zeilenumbruch', 'abc\n'], ['Umlaut', 'Jürgen'], ['Emoji', 'abc😀'], ['Klammeraffe', '@kenso'], ['Schrägstrich', 'a/b'], ]; for (const [label, name] of cases) expectError(await profile(name), 400, 'usernameInvalid', label); assert.equal(fake.rows('profiles').length, 0); assert.equal(nonAccountCalls(fake).length, 0); }); test('kein Text als Name (fehlt, null, Zahl, Liste, Objekt, Wahrheitswert): 400 usernameInvalid; kein JSON-Objekt: 400 badRequest', async () => { const { post, call, fake } = await setup(); for (const body of [{}, { username: null }, { username: 7 }, { username: ['abc'] }, { username: { a: 1 } }, { username: true }, { name: 'Kenso' }]) { expectError(await post('/api/profil', body, JWT_ME), 400, 'usernameInvalid', JSON.stringify(body)); } for (const raw of ['[]', '"abc"', '7', '{kaputt']) { const res = await call('POST', '/api/profil', { raw, headers: { 'X-Aircard-Jwt': JWT_ME } }); expectError(res, 400, 'badRequest', raw); } assert.equal(fake.rows('profiles').length, 0); }); test('ohne oder mit ungültigem JWT: 401, noch vor der Prüfung des Namens, nichts angelegt', async () => { const { post, profile, fake } = await setup(); expectError(await post('/api/profil', { username: 'Kenso' }), 401, 'unauthorized', 'ohne JWT'); expectError(await post('/api/profil', {}), 401, 'unauthorized', 'ohne JWT, ungültiger Body'); expectError(await profile('Kenso', 'xxx.yyy.zzz'), 401, 'unauthorized', 'unbekanntes JWT'); expectError(await profile('Kenso', 'zwei teile'), 401, 'unauthorized', 'unzulässige Zeichen'); expectError(await profile('Admin', ''), 401, 'unauthorized', 'leeres JWT'); assert.equal(fake.rows('profiles').length, 0); assert.equal(nonAccountCalls(fake).length, 0, 'ohne gültiges JWT wird nichts abgefragt oder geschrieben'); }); test('nur POST: GET /api/profil gibt es nicht (404)', async () => { const { call } = await setup(); expectError(await call('GET', '/api/profil'), 404, 'notFound'); }); // ---- Fehler bei Appwrite ----------------------------------------------------------------------------------- test('Appwrite antwortet mit Fehler: 502 unavailable ohne halben Zustand, der nächste Versuch gelingt', async () => { const { profile, fake } = await setup(); // die Vorabprüfung scheitert fake.failNext(isList, { status: 500 }); expectError(await profile('Kenso'), 502, 'unavailable', 'Vorabprüfung'); assert.equal(fake.rows('profiles').length, 0); // das Anlegen scheitert fake.failNext(isCreate, { status: 503 }); expectError(await profile('Kenso'), 502, 'unavailable', 'Anlegen'); assert.equal(fake.rows('profiles').length, 0); assert.deepEqual((await profile('Kenso')).json, { username: 'Kenso' }); // das Ändern scheitert: das alte Profil bleibt fake.failNext(isUpdate, { status: 500 }); expectError(await profile('Neu.name'), 502, 'unavailable', 'Ändern'); assert.equal(fake.row('profiles', ME).username, 'Kenso'); assert.deepEqual((await profile('Neu.name')).json, { username: 'Neu.name' }); assert.equal(fake.row('profiles', ME).usernameKey, 'neu.name'); }); test('fehlt die Tabelle `profiles` (Schema nicht eingespielt): 500 internal, kein Absturz', async () => { const { profile, fake } = await setup(); fake.failNext(isList, { status: 404, type: 'table_not_found' }); expectError(await profile('Kenso'), 500, 'internal'); }); // ---- Logs ----------------------------------------------------------------------------------------------------- test('Logs: eine Ereigniszeile je Anlegen und Ändern, nie Name, Konto-ID, Schlüssel oder JWT', async () => { const { profile, logs, fake } = await setup(); await profile('GEHEIMER.Name'); // anlegen await profile('GEHEIMER.Neu'); // ändern await profile('ab'); // 400 await profile('Admin'); // 400 await profile('geheimer.neu', JWT_OTHER); // 409 await profile('x', 'xxx.yyy.zzz'); // 401 const lines = logs.map((line) => line.replace(/^\[aircard-api\] \S+ /, '')); assert.deepEqual(lines, ['profile created', 'profile updated']); assert.ok(logs.every((line) => line.startsWith('[aircard-api] '))); for (const secret of [fake.key, SALT, JWT_ME, JWT_OTHER, 'GEHEIMER', 'geheimer', ME, OTHER]) { assert.ok(!logs.join('\n').includes(secret), `Log enthält „${secret}“`); } });