feat(api): Benutzernamen (Profil-Tabelle, /api/profil, Pflicht beim Hochladen und Veröffentlichen)
- Schema: Tabelle `profiles` (Zeilen-ID = Konto-ID, `username` und `usernameKey`
je 20 Zeichen, Unique-Index `username_key` auf `usernameKey`, rowSecurity an,
keine Tabellenrechte) im Setup-Skript.
- Neues Modul `deploy/api/profiles.js`: `POST /api/profil` legt das Profil an
oder ändert es (Zeilenrecht genau read("any"), Sperre je Konto). Muster,
Sperrliste und Schlüssel (`usernameKey`) stehen in `common.js`; vergeben ->
409 `usernameTaken`, auch wenn erst der Unique-Index bei einem Rennen
entscheidet. Dazu die MSG-Texte `usernameInvalid`, `usernameReserved`,
`usernameTaken`, `usernameRequired`.
- `POST /api/logos/hochladen` und `POST /api/veroeffentlichen` antworten 403
`usernameRequired`, wenn das Konto kein Profil hat (`hasProfile`, nach der
Anmeldung und vor allen anderen Prüfungen).
- Fake-Appwrite: Tabelle `profiles` mit Unique-Index (409), `addProfile`,
`holdNext`. API-Dienst besteht jetzt aus sechs Dateien (READMEs).
- Client: die vier API-Texte in `API_MESSAGES` und `en.ts` (Gleichlauf mit MSG).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a65c075004
commit
19c8151c7e
15 files changed
+980
-53
No files matched your search
@@ -27,9 +27,12 @@ const SALT = 'test-salt-0123456789abcdef0123456789abcdef';
|
||||
const ME = 'user-me';
|
||||
const OTHER = 'user-other';
|
||||
const ADMIN = 'user-admin';
|
||||
/** Konto ohne Benutzernamen (alle anderen haben aus `setup` ein Profil): Hochladen geht erst mit Profil. */
|
||||
const NEWBIE = 'user-new';
|
||||
const JWT_ME = 'eyJhbGciOiJI.payload-me.signature-me';
|
||||
const JWT_OTHER = 'eyJhbGciOiJI.payload-other.signature-other';
|
||||
const JWT_ADMIN = 'eyJhbGciOiJI.payload-admin.signature-admin';
|
||||
const JWT_NEW = 'eyJhbGciOiJI.payload-new.signature-new';
|
||||
const MIB = 1024 * 1024;
|
||||
const LOGO_BUCKET = 'logo-images';
|
||||
/** Rechte einer frisch hochgeladenen Logo-Datei von ME. */
|
||||
@@ -48,6 +51,9 @@ async function setup() {
|
||||
fake.addUser(JWT_ME, { id: ME });
|
||||
fake.addUser(JWT_OTHER, { id: OTHER });
|
||||
fake.addUser(JWT_ADMIN, { id: ADMIN, labels: ['admin'] });
|
||||
fake.addUser(JWT_NEW, { id: NEWBIE });
|
||||
// Hochladen verlangt einen Benutzernamen (Tabelle `profiles`): ME, OTHER und ADMIN haben einen, NEWBIE nicht.
|
||||
for (const id of [ME, OTHER, ADMIN]) fake.addProfile(id);
|
||||
|
||||
const logs = [];
|
||||
const clock = { value: 1_700_000_000_000 };
|
||||
@@ -159,6 +165,8 @@ function seedReports(fake, logoId, hashes) {
|
||||
|
||||
const sha32 = (text) => crypto.createHash('sha256').update(text).digest('hex').slice(0, 32);
|
||||
const nonAccountCalls = (fake) => fake.calls.filter((c) => c.path !== '/v1/account');
|
||||
/** Die Aufrufe bei Appwrite außer dem Konto und der Suche nach dem Profil von ME (die macht jede Anfrage zum Hochladen als Erstes). */
|
||||
const callsBeyondProfile = (fake) => nonAccountCalls(fake).filter((c) => !(c.method === 'GET' && c.path.endsWith('/tables/profiles/rows/user-me')));
|
||||
const filePuts = (fake, id) => fake.calls.filter((c) => c.method === 'PUT' && c.path.endsWith(`/files/${id}`));
|
||||
const fileReads = (fake, id) => fake.calls.filter((c) => c.method === 'GET' && c.path.endsWith(`/files/${id}/view`));
|
||||
|
||||
@@ -319,6 +327,47 @@ test('Logo hochladen ohne oder mit ungültigem JWT: 401, nichts angelegt, die Da
|
||||
assert.equal(fake.calls.filter((c) => c.path === '/v1/account').length, 1, 'fehlendes oder kaputtes JWT erreicht Appwrite gar nicht erst');
|
||||
});
|
||||
|
||||
test('Logo hochladen ohne Benutzernamen: 403 usernameRequired noch vor jeder anderen Prüfung (auch vor Body und Datei), nichts angelegt, Datei unberührt', async () => {
|
||||
const { upload, post, fake } = await setup();
|
||||
logoFile(fake, 'logo1', NEWBIE);
|
||||
expectError(await upload(uploadBody(), JWT_NEW), 403, 'usernameRequired', 'gültiger Body');
|
||||
expectError(await upload({}, JWT_NEW), 403, 'usernameRequired', 'ungültiger Body (kein Name, keine Datei)');
|
||||
expectError(await upload(uploadBody({ fileId: 'gibts-nicht' }), JWT_NEW), 403, 'usernameRequired', 'Datei gibt es nicht');
|
||||
expectError(await upload(uploadBody({ fileId: 'logo1', mono: 'ja' }), JWT_NEW), 403, 'usernameRequired', 'mono ungültig');
|
||||
expectError(await post('/api/logos/hochladen', null, JWT_NEW), 403, 'usernameRequired', 'kein Body');
|
||||
|
||||
assert.equal(fake.rows('logos').length, 0);
|
||||
assert.deepEqual(fake.file('logo1').$permissions, ['read("any")', 'delete("user:user-new")'], 'die Datei bleibt, wie sie war');
|
||||
assert.equal(fake.calls.filter((c) => c.path.startsWith('/v1/storage/')).length, 0, 'keine Datei wird angefasst');
|
||||
// Appwrite sieht je Versuch nur die Prüfung des JWT und die Suche nach dem Profil (Zeilen-ID = Konto-ID)
|
||||
assert.deepEqual(
|
||||
nonAccountCalls(fake).map((c) => `${c.method} ${c.path}`),
|
||||
Array(5).fill('GET /v1/tablesdb/aircard/tables/profiles/rows/user-new'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Logo hochladen: das Profil gehört dem Konto – ein Profil der anderen Konten hilft nicht, mit eigenem Profil (über /api/profil) geht es', async () => {
|
||||
const { upload, post, fake } = await setup();
|
||||
logoFile(fake, 'logo1', NEWBIE);
|
||||
expectError(await upload(uploadBody(), JWT_NEW), 403, 'usernameRequired');
|
||||
|
||||
const named = await post('/api/profil', { username: 'Neuling' }, JWT_NEW);
|
||||
assert.deepEqual(named.json, { username: 'Neuling' });
|
||||
const res = await upload(uploadBody(), JWT_NEW);
|
||||
assert.equal(res.status, 201);
|
||||
assert.equal(fake.row('logos', rowOf('logo1', NEWBIE)).ownerId, NEWBIE);
|
||||
});
|
||||
|
||||
test('Logo hochladen: lässt sich das Profil nicht prüfen (Appwrite-Fehler), 502 und nichts wird angelegt; der nächste Versuch gelingt', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
logoFile(fake, 'logo1');
|
||||
fake.failNext((c) => c.method === 'GET' && c.path.endsWith('/tables/profiles/rows/user-me'), { status: 500 });
|
||||
expectError(await upload(uploadBody()), 502, 'unavailable');
|
||||
assert.equal(fake.rows('logos').length, 0);
|
||||
assert.deepEqual(fake.file('logo1').$permissions, UPLOAD_PERMISSIONS);
|
||||
assert.equal((await upload(uploadBody())).status, 201);
|
||||
});
|
||||
|
||||
test('Logo hochladen mit fremder, falscher oder gesperrter Datei: 403, keine Zeile, Datei unverändert, Inhalt wird nicht gelesen', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
logoFile(fake, 'fremd', OTHER);
|
||||
@@ -698,7 +747,7 @@ test('Logo hochladen: scheitert das Aufräumen nach einer Beanstandung, steht `r
|
||||
assert.equal(fake.row('logos', rowOf('logo2')), null);
|
||||
});
|
||||
|
||||
test('Logo hochladen: Validierung liefert 400 mit dem passenden Code und fasst Appwrite (außer dem Konto) nicht an', async () => {
|
||||
test('Logo hochladen: Validierung liefert 400 mit dem passenden Code und fasst Appwrite (außer Konto und Profil) nicht an', async () => {
|
||||
const { upload, fake } = await setup();
|
||||
logoFile(fake, 'logo1');
|
||||
|
||||
@@ -741,7 +790,7 @@ test('Logo hochladen: Validierung liefert 400 mit dem passenden Code und fasst A
|
||||
}
|
||||
|
||||
assert.equal(fake.rows('logos').length, 0);
|
||||
assert.equal(nonAccountCalls(fake).length, 0, 'ungültige Anfragen erreichen Appwrite (außer dem Konto) nie');
|
||||
assert.equal(callsBeyondProfile(fake).length, 0, 'ungültige Anfragen erreichen Appwrite (außer Konto und Profil) nie');
|
||||
});
|
||||
|
||||
test('Logo hochladen: kaputtes JSON, Array oder leerer Body ist 400 badRequest', async () => {
|
||||
@@ -750,7 +799,7 @@ test('Logo hochladen: kaputtes JSON, Array oder leerer Body ist 400 badRequest',
|
||||
const res = await call('POST', '/api/logos/hochladen', { raw, headers: { 'X-Aircard-Jwt': JWT_ME } });
|
||||
expectError(res, 400, 'badRequest', JSON.stringify(raw));
|
||||
}
|
||||
assert.equal(nonAccountCalls(fake).length, 0);
|
||||
assert.equal(callsBeyondProfile(fake).length, 0);
|
||||
});
|
||||
|
||||
test('Logo hochladen: gleichzeitig dieselbe Datei oder zwei Dateien am Limit, genau eine Anfrage gelingt (Sperre je Konto)', async () => {
|
||||
|
||||
Reference in new issue
Block a user