Drei neue Ende-zu-Ende-Tests: das Verwalten mehrerer Repositories samt Datentrennung, das Lesen von Exportdateien der Version 1, und die Uebernahme ohne verknuepftes Repository. Dabei fiel auf, dass der Uebernahme-Endpunkt ohne Repository mit 404 antwortete, obwohl gar keine Repository-ID angefragt war. Wer nichts angibt, hat schlicht noch keines verknuepft - das ist kein "nicht gefunden", sondern 409 mit dem Hinweis, eines zu hinterlegen. Version 1.0.0: der Funktionsumfang ist beisammen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,15 +69,31 @@ SSRF-Schutz (`lib/security/ssrf.ts`).
|
|||||||
`prisma migrate dev` ist hier nicht benutzbar (interaktiv). Stattdessen:
|
`prisma migrate dev` ist hier nicht benutzbar (interaktiv). Stattdessen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx prisma migrate diff --from-schema-datasource prisma/schema.prisma \
|
docker exec nebula-db psql -U nebula -d postgres -c "CREATE DATABASE nebula_shadow"
|
||||||
--to-schema-datamodel prisma/schema.prisma --script --output prisma/migrations/<stempel>_<name>/migration.sql
|
npx prisma migrate diff --from-migrations prisma/migrations \
|
||||||
|
--to-schema-datamodel prisma/schema.prisma \
|
||||||
|
--shadow-database-url postgresql://nebula:nebula@localhost:5432/nebula_shadow \
|
||||||
|
--script --output prisma/migrations/<stempel>_<name>/migration.sql
|
||||||
npx prisma migrate deploy
|
npx prisma migrate deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`--from-migrations`, nicht `--from-schema-datasource`.** Letzteres
|
||||||
|
vergleicht mit der *laufenden* Datenbank. Ist eine eben erzeugte Migration
|
||||||
|
dort noch nicht eingespielt, steht ihr Inhalt in der nächsten gleich noch
|
||||||
|
einmal.
|
||||||
|
|
||||||
**Die erzeugte SQL vor dem Anwenden lesen.** Der Volltextindex
|
**Die erzeugte SQL vor dem Anwenden lesen.** Der Volltextindex
|
||||||
`Note_fulltext_idx` ist ein Ausdrucksindex, den Prisma nicht kennt (er wird
|
`Note_fulltext_idx` ist ein Ausdrucksindex, den Prisma nicht kennt (er wird
|
||||||
bislang in Ruhe gelassen, aber das ist nicht garantiert).
|
bislang in Ruhe gelassen, aber das ist nicht garantiert).
|
||||||
|
|
||||||
|
**Datenmigrationen von Hand schreiben.** `migrate diff` kennt nur den
|
||||||
|
Zielzustand: Spalten, die woandershin wandern, würde es ersatzlos löschen.
|
||||||
|
Vorgehen wie in `20260726230000_repositories`: neue Tabelle anlegen, Bestand
|
||||||
|
per `INSERT … SELECT` übernehmen, dann erst die alten Spalten entfernen.
|
||||||
|
Danach `migrate diff` erneut laufen lassen — kommt „empty migration“ heraus,
|
||||||
|
stimmt das Ergebnis mit dem Schema überein. Und gegen eine Kopie mit echten
|
||||||
|
Daten prüfen, nicht nur gegen eine leere Datenbank.
|
||||||
|
|
||||||
## Prüfen, nicht behaupten
|
## Prüfen, nicht behaupten
|
||||||
|
|
||||||
Vor „fertig": `npx tsc --noEmit`, `npm test`, `npm run build`.
|
Vor „fertig": `npx tsc --noEmit`, `npm test`, `npm run build`.
|
||||||
@@ -86,6 +102,19 @@ Bei Änderungen an der Oberfläche **hinsehen**. Chrome ist installiert, ein
|
|||||||
Skript mit `playwright-core` und `channel: "chrome"` genügt für Screenshots.
|
Skript mit `playwright-core` und `channel: "chrome"` genügt für Screenshots.
|
||||||
Mehrere Anzeigefehler dieses Projekts sind ausschließlich so aufgefallen.
|
Mehrere Anzeigefehler dieses Projekts sind ausschließlich so aufgefallen.
|
||||||
|
|
||||||
|
Wer den Standalone-Bau von Hand startet (`node .next/standalone/server.js`),
|
||||||
|
muss vorher `.next/static` und `public` danebenkopieren — sonst lädt kein
|
||||||
|
JavaScript, die Seite bleibt serverseitig gerendert, und Formulare senden
|
||||||
|
sich nativ ab. Das sieht aus wie ein Anwendungsfehler, ist aber keiner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/
|
||||||
|
```
|
||||||
|
|
||||||
|
`pkill` gibt es in dieser Git-Bash nicht. Prozesse über PowerShell beenden
|
||||||
|
(`Get-CimInstance Win32_Process … | Stop-Process`) — sonst läuft der alte
|
||||||
|
Server weiter, hält die Prisma-DLL und blockiert den nächsten Bau.
|
||||||
|
|
||||||
**Niemals gegen die echte Datenbank testen.** Eine Wegwerf-Datenbank anlegen
|
**Niemals gegen die echte Datenbank testen.** Eine Wegwerf-Datenbank anlegen
|
||||||
und danach löschen:
|
und danach löschen:
|
||||||
|
|
||||||
|
|||||||
+43
-6
@@ -27,6 +27,14 @@ sonst würden Konten ausgesperrt. Nach der Einrichtung ist `/setup` dauerhaft ge
|
|||||||
- **Passkey (WebAuthn).** Fingerabdruck, Gesichtserkennung oder Sicherheitsschlüssel. Die
|
- **Passkey (WebAuthn).** Fingerabdruck, Gesichtserkennung oder Sicherheitsschlüssel. Die
|
||||||
Anmeldung läuft über auffindbare Credentials, ein Benutzername ist also nicht nötig.
|
Anmeldung läuft über auffindbare Credentials, ein Benutzername ist also nicht nötig.
|
||||||
Mehrere Passkeys pro Konto, benennbar und einzeln entfernbar.
|
Mehrere Passkeys pro Konto, benennbar und einzeln entfernbar.
|
||||||
|
- **Zwei-Faktor per TOTP.** Zeitbasiertes Einmalkennwort zusätzlich zum Passwort, für jede
|
||||||
|
gängige Authenticator-App. Einrichtung über QR-Code oder abgetippten Schlüssel; scharf
|
||||||
|
wird er erst nach einem bestätigten Code, damit sich niemand aussperrt, der den Scan
|
||||||
|
nicht hinbekommt. Dazu zehn **Wiederherstellungscodes**, jeder genau einmal gültig —
|
||||||
|
sie werden nur bei der Ausgabe angezeigt. Abschalten und Neuerzeugen verlangen das
|
||||||
|
Passwort, nicht bloß eine offene Sitzung.
|
||||||
|
- Die Passkey-Anmeldung fragt **nicht** nach dem zweiten Faktor: der Passkey ist bereits an
|
||||||
|
ein Gerät gebunden und damit selbst der zweite Faktor.
|
||||||
- Ein Konto kann **ausschließlich** per Passkey nutzbar sein. Der letzte Passkey lässt sich
|
- Ein Konto kann **ausschließlich** per Passkey nutzbar sein. Der letzte Passkey lässt sich
|
||||||
dann nicht entfernen, solange kein Passwort gesetzt ist.
|
dann nicht entfernen, solange kein Passwort gesetzt ist.
|
||||||
- Fehlversuche werden gezählt; nach acht sperrt sich das Konto für 15 Minuten.
|
- Fehlversuche werden gezählt; nach acht sperrt sich das Konto für 15 Minuten.
|
||||||
@@ -35,9 +43,9 @@ sonst würden Konten ausgesperrt. Nach der Einrichtung ist `/setup` dauerhaft ge
|
|||||||
|
|
||||||
### 0.3 Mein Konto
|
### 0.3 Mein Konto
|
||||||
|
|
||||||
Anzeigename und E-Mail ändern · Passwort setzen oder wechseln · Passkeys verwalten ·
|
Anzeigename und E-Mail ändern · Passwort setzen oder wechseln · Zwei-Faktor einrichten und
|
||||||
aktive Sitzungen einsehen (Browser, Betriebssystem, IP, letzte Aktivität) · überall
|
abschalten · Passkeys verwalten · aktive Sitzungen einsehen (Browser, Betriebssystem, IP,
|
||||||
abmelden, wobei das eigene Gerät angemeldet bleibt.
|
letzte Aktivität) · überall abmelden, wobei das eigene Gerät angemeldet bleibt.
|
||||||
|
|
||||||
### 0.4 Benutzerverwaltung (Administratoren)
|
### 0.4 Benutzerverwaltung (Administratoren)
|
||||||
|
|
||||||
@@ -215,6 +223,31 @@ statt verschoben.
|
|||||||
|
|
||||||
## 4. Git-Anbindung
|
## 4. Git-Anbindung
|
||||||
|
|
||||||
|
### 4.0 Mehrere Repositories je Projekt
|
||||||
|
|
||||||
|
Ein Vorhaben besteht oft aus mehreren Repositories — Frontend und Backend etwa, oder
|
||||||
|
App und Infrastruktur. Sie gehören trotzdem in **eine** Karte.
|
||||||
|
|
||||||
|
- Im Git-Panel steht eine Leiste mit allen Repositories des Projekts, sobald es mehr als
|
||||||
|
eines gibt. Jedes hat eigene Kennzahlen, Listen, CI und eigenen Abgleich; „Alle" gleicht
|
||||||
|
sie nacheinander ab — nicht gleichzeitig, denn ein Schwung Anfragen an denselben Anbieter
|
||||||
|
ist genau das, worauf Rate-Limits reagieren.
|
||||||
|
- Eine frei wählbare **Bezeichnung** („Frontend") tritt an die Stelle von `owner/name`.
|
||||||
|
- Das erste Repository ist das **führende**. Wo nur eines dargestellt werden kann — auf der
|
||||||
|
Karte im Dashboard, im Kanban —, ist es dieses. Die Karte zeigt daneben `+2 Repos`.
|
||||||
|
Ein Abgleichsfehler an *irgendeinem* Repository erscheint auf der Karte, sonst bliebe er
|
||||||
|
unbemerkt.
|
||||||
|
- **Stillstand** bemisst sich am jüngsten Commit über alle Repositories: an einem Vorhaben
|
||||||
|
wird gearbeitet, sobald an einem seiner Teile gearbeitet wird.
|
||||||
|
- Benachrichtigungen merken sich den gemeldeten Zustand **je Repository**. Sonst verdeckte
|
||||||
|
ein rotes Frontend die rote Pipeline des Backends. Der Betreff nennt das Repository nur,
|
||||||
|
wenn es nicht ohnehin so heißt wie das Projekt.
|
||||||
|
- Übernommene Issues merken sich, aus welchem Repository sie stammen — Frontend und Backend
|
||||||
|
haben beide ein Issue Nummer 5, und das sind zwei Aufgaben.
|
||||||
|
|
||||||
|
Der Projektdialog bearbeitet weiterhin nur das führende Repository; weitere verwaltet das
|
||||||
|
Git-Panel. Beim Anlegen eines Projekts entsteht aus den Git-Feldern das erste.
|
||||||
|
|
||||||
### 4.1 Unterstützte Systeme
|
### 4.1 Unterstützte Systeme
|
||||||
|
|
||||||
| System | API | Authentifizierung | Besonderheit |
|
| System | API | Authentifizierung | Besonderheit |
|
||||||
@@ -441,13 +474,17 @@ letzten Zustellung stehen an der Karte.
|
|||||||
| `Session` | Serverseitige Sitzungen (nur der Token-Hash) |
|
| `Session` | Serverseitige Sitzungen (nur der Token-Hash) |
|
||||||
| `Credential` | Passkeys inkl. öffentlichem Schlüssel und Signaturzähler |
|
| `Credential` | Passkeys inkl. öffentlichem Schlüssel und Signaturzähler |
|
||||||
| `WebAuthnChallenge` | Kurzlebige, einmalig verwendbare Challenges |
|
| `WebAuthnChallenge` | Kurzlebige, einmalig verwendbare Challenges |
|
||||||
|
| `RecoveryCode` | Wiederherstellungscodes für die Zwei-Faktor-Anmeldung (nur der Hash) |
|
||||||
|
| `PendingLogin` | Zwischenzustand zwischen Passwort und zweitem Faktor, fünf Minuten gültig |
|
||||||
| `AppSettings` | Betriebsart und Registrierung (eine Zeile) |
|
| `AppSettings` | Betriebsart und Registrierung (eine Zeile) |
|
||||||
| `Project` | Projektstammdaten, Status, Fortschritt, Repo-Verknüpfung, Besitzer |
|
| `Project` | Projektstammdaten, Status, Fortschritt, Besitzer |
|
||||||
| `Task` | Aufgaben mit Spalte, Position, Fälligkeit und Labels |
|
| `Repository` | Repositories eines Projekts inkl. Abgleichstand (n:1 zu Projekt) |
|
||||||
|
| `Task` | Aufgaben mit Spalte, Position, Fälligkeit, Labels und Issue-Herkunft |
|
||||||
|
| `Activity` | Verlauf je Projekt, neueste zuerst, auf 200 Einträge begrenzt |
|
||||||
| `NotificationTarget` | Zustellwege je Konto, mit verschlüsseltem Geheimnis |
|
| `NotificationTarget` | Zustellwege je Konto, mit verschlüsseltem Geheimnis |
|
||||||
| `Note` | Notizen (n:1 zu Projekt, Cascade-Delete) |
|
| `Note` | Notizen (n:1 zu Projekt, Cascade-Delete) |
|
||||||
| `GitConnection` | Git-Systeme inkl. verschlüsseltem Token, Besitzer |
|
| `GitConnection` | Git-Systeme inkl. verschlüsseltem Token, Besitzer |
|
||||||
| `RepoSnapshot` | Zwischengespeicherter Repo-Stand (1:1 zu Projekt) |
|
| `RepoSnapshot` | Zwischengespeicherter Repo-Stand (1:1 zu Repository) |
|
||||||
|
|
||||||
`Project.ownerId` und `GitConnection.ownerId` sind bewusst optional: so überleben Daten aus
|
`Project.ownerId` und `GitConnection.ownerId` sind bewusst optional: so überleben Daten aus
|
||||||
der Zeit vor der Benutzerverwaltung die Migration und werden beim Anlegen des ersten Kontos
|
der Zeit vor der Benutzerverwaltung die Migration und werden beim Anlegen des ersten Kontos
|
||||||
|
|||||||
@@ -324,12 +324,15 @@ ausschließlich Daten des angemeldeten Kontos.
|
|||||||
| `GET` | `/api/health` | Status inkl. Datenbankverbindung (Docker-Healthcheck) |
|
| `GET` | `/api/health` | Status inkl. Datenbankverbindung (Docker-Healthcheck) |
|
||||||
| `POST` | `/api/auth/setup` | Administratorkonto anlegen (nur beim ersten Mal) |
|
| `POST` | `/api/auth/setup` | Administratorkonto anlegen (nur beim ersten Mal) |
|
||||||
| `POST` | `/api/auth/login` `/api/auth/logout` `/api/auth/register` | Anmeldung |
|
| `POST` | `/api/auth/login` `/api/auth/logout` `/api/auth/register` | Anmeldung |
|
||||||
|
| `POST` | `/api/auth/mfa` | Zweiter Faktor: Einmalkennwort oder Wiederherstellungscode |
|
||||||
| `GET` | `/api/auth/session` | Aktuelle Sitzung und Instanzzustand |
|
| `GET` | `/api/auth/session` | Aktuelle Sitzung und Instanzzustand |
|
||||||
| `POST` | `/api/auth/passkey/{login,register}/{options,verify}` | WebAuthn |
|
| `POST` | `/api/auth/passkey/{login,register}/{options,verify}` | WebAuthn |
|
||||||
| `PATCH` | `/api/account/profile` | Eigenes Profil |
|
| `PATCH` | `/api/account/profile` | Eigenes Profil |
|
||||||
| `POST` | `/api/account/password` | Eigenes Passwort setzen |
|
| `POST` | `/api/account/password` | Eigenes Passwort setzen |
|
||||||
| `GET` `DELETE` | `/api/account/sessions` | Eigene Sitzungen / überall abmelden |
|
| `GET` `DELETE` | `/api/account/sessions` | Eigene Sitzungen / überall abmelden |
|
||||||
| `PATCH` `DELETE` | `/api/account/passkeys/:id` | Passkey umbenennen / entfernen |
|
| `PATCH` `DELETE` | `/api/account/passkeys/:id` | Passkey umbenennen / entfernen |
|
||||||
|
| `GET` `POST` `PUT` `DELETE` | `/api/account/totp` | Zwei-Faktor: Zustand, einrichten, aktivieren, abschalten |
|
||||||
|
| `POST` | `/api/account/totp/recovery` | Neue Wiederherstellungscodes |
|
||||||
| `GET` `PATCH` | `/api/admin/settings` | Betriebsart, Registrierung, Abgleich (Admin) |
|
| `GET` `PATCH` | `/api/admin/settings` | Betriebsart, Registrierung, Abgleich (Admin) |
|
||||||
| `POST` | `/api/admin/sync/run` | Abgleich sofort anstoßen (Admin) |
|
| `POST` | `/api/admin/sync/run` | Abgleich sofort anstoßen (Admin) |
|
||||||
| `GET` `POST` | `/api/admin/users` | Konten auflisten / anlegen (Admin) |
|
| `GET` `POST` | `/api/admin/users` | Konten auflisten / anlegen (Admin) |
|
||||||
@@ -344,7 +347,11 @@ ausschließlich Daten des angemeldeten Kontos.
|
|||||||
| `PATCH` `DELETE` | `/api/notifications/:id` | Ziel ändern / entfernen |
|
| `PATCH` `DELETE` | `/api/notifications/:id` | Ziel ändern / entfernen |
|
||||||
| `POST` | `/api/notifications/:id/test` | Testnachricht senden |
|
| `POST` | `/api/notifications/:id/test` | Testnachricht senden |
|
||||||
| `GET` `PATCH` `DELETE` | `/api/projects/:id` | Projekt lesen / ändern / löschen |
|
| `GET` `PATCH` `DELETE` | `/api/projects/:id` | Projekt lesen / ändern / löschen |
|
||||||
| `POST` | `/api/projects/:id/sync` | Repo-Daten vom Git-System holen |
|
| `POST` | `/api/projects/:id/sync` | Alle Repositories des Projekts abgleichen |
|
||||||
|
| `GET` | `/api/projects/:id/activity` | Verlauf des Projekts (`?limit=`) |
|
||||||
|
| `GET` `POST` | `/api/projects/:id/repositories` | Repositories des Projekts |
|
||||||
|
| `PATCH` `DELETE` | `/api/repositories/:id` | Repository ändern / vom Projekt lösen |
|
||||||
|
| `POST` | `/api/repositories/:id/sync` | Ein einzelnes Repository abgleichen |
|
||||||
| `GET` `POST` | `/api/projects/:id/tasks` | Aufgaben des Projekts |
|
| `GET` `POST` | `/api/projects/:id/tasks` | Aufgaben des Projekts |
|
||||||
| `PATCH` | `/api/projects/:id/tasks/reorder` | Neue Reihenfolge nach dem Ziehen |
|
| `PATCH` | `/api/projects/:id/tasks/reorder` | Neue Reihenfolge nach dem Ziehen |
|
||||||
| `POST` | `/api/projects/:id/tasks/from-issues` | Offene Issues als Aufgaben übernehmen |
|
| `POST` | `/api/projects/:id/tasks/from-issues` | Offene Issues als Aufgaben übernehmen |
|
||||||
|
|||||||
+2
-12
@@ -37,17 +37,7 @@ Frei sortieren hieße, eine Position je Projekt zu speichern.
|
|||||||
|
|
||||||
## 3 · Größere Brocken
|
## 3 · Größere Brocken
|
||||||
|
|
||||||
### 3.1 Mehrere Repositories je Projekt `M`
|
### 3.1 Passwort-Zurücksetzen per E-Mail `M`
|
||||||
|
|
||||||
Frontend und Backend getrennt, aber ein Projekt. Erfordert eine Zwischentabelle
|
|
||||||
und Anpassungen in der Anzeige.
|
|
||||||
|
|
||||||
### 3.2 Zwei-Faktor per TOTP `M`
|
|
||||||
|
|
||||||
Als Alternative zum Passkey — nützlich, wenn die Instanz aus dem Internet
|
|
||||||
erreichbar ist und nicht jedes Gerät Passkeys kann.
|
|
||||||
|
|
||||||
### 3.3 Passwort-Zurücksetzen per E-Mail `M`
|
|
||||||
|
|
||||||
Heute setzt ein Administrator das Passwort neu. Selbstbedienung braucht SMTP
|
Heute setzt ein Administrator das Passwort neu. Selbstbedienung braucht SMTP
|
||||||
und damit eine weitere Konfiguration — lohnt erst im Mehrbenutzerbetrieb.
|
und damit eine weitere Konfiguration — lohnt erst im Mehrbenutzerbetrieb.
|
||||||
@@ -65,7 +55,6 @@ Voraussetzung, falls du das Projekt veröffentlichen willst.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste |
|
| Suchtreffer in Notizen hervorheben | `S` | In der Notiz selbst, nicht nur in der Trefferliste |
|
||||||
| Mehrfachauswahl im Board | `S` | Status oder Tags für mehrere auf einmal |
|
| Mehrfachauswahl im Board | `S` | Status oder Tags für mehrere auf einmal |
|
||||||
| Aktivitätsverlauf je Projekt | `S` | Wer hat wann was geändert |
|
|
||||||
| Wiederkehrende Aufgaben | `M` | Etwa „jeden Montag Abhängigkeiten prüfen" |
|
| Wiederkehrende Aufgaben | `M` | Etwa „jeden Montag Abhängigkeiten prüfen" |
|
||||||
| Projektvorlagen | `S` | Neues Projekt mit vorgefüllten Tags und Notizen |
|
| Projektvorlagen | `S` | Neues Projekt mit vorgefüllten Tags und Notizen |
|
||||||
| Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel |
|
| Hell/Dunkel umschaltbar | `M` | Aktuell nur dunkel |
|
||||||
@@ -111,6 +100,7 @@ Damit klar ist, was **nicht** kommt und warum:
|
|||||||
| **v0.7.0** Benachrichtigungen über ntfy, Webhook und E-Mail | ✓ |
|
| **v0.7.0** Benachrichtigungen über ntfy, Webhook und E-Mail | ✓ |
|
||||||
| **v0.8.0** Stillstehende Projekte melden, Schwelle im Adminbereich | ✓ |
|
| **v0.8.0** Stillstehende Projekte melden, Schwelle im Adminbereich | ✓ |
|
||||||
| **v0.9.0** Schnellsuche per `Strg+K`, Aufgaben aus Git-Issues | ✓ |
|
| **v0.9.0** Schnellsuche per `Strg+K`, Aufgaben aus Git-Issues | ✓ |
|
||||||
|
| **v1.0.0** Aktivitätsverlauf je Projekt, Zwei-Faktor per TOTP, mehrere Repositories je Projekt | ✓ |
|
||||||
|
|
||||||
Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur
|
Zur Entscheidung beim Export: Git-Tokens gehen **nicht** mit. Die Verbindung wird nur
|
||||||
beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben
|
beschrieben, das Token ist nach dem Import neu zu hinterlegen. Snapshots bleiben
|
||||||
|
|||||||
+121
-1
@@ -191,11 +191,92 @@ test("Issues als Aufgaben uebernehmen, ohne Dubletten", async ({ request }) => {
|
|||||||
await (await request.post("/api/projects", { data: { name: "Issue-Projekt" } })).json()
|
await (await request.post("/api/projects", { data: { name: "Issue-Projekt" } })).json()
|
||||||
).project;
|
).project;
|
||||||
|
|
||||||
// Ohne Repo-Stand gibt es nichts zu uebernehmen – und das sagt der Server auch.
|
// Ohne verknuepftes Repository gibt es nichts zu uebernehmen.
|
||||||
|
const ohneRepo = await request.post(`/api/projects/${projekt.id}/tasks/from-issues`, {
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
expect(ohneRepo.status()).toBe(409);
|
||||||
|
|
||||||
|
// Mit Repository, aber ohne geholten Stand ebenfalls nicht – der Server
|
||||||
|
// laedt hier bewusst nichts nach.
|
||||||
|
await request.patch(`/api/projects/${projekt.id}`, {
|
||||||
|
data: { repoUrl: "https://github.com/vercel/swr" },
|
||||||
|
});
|
||||||
const ohneStand = await request.post(`/api/projects/${projekt.id}/tasks/from-issues`, {
|
const ohneStand = await request.post(`/api/projects/${projekt.id}/tasks/from-issues`, {
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
expect(ohneStand.status()).toBe(409);
|
expect(ohneStand.status()).toBe(409);
|
||||||
|
|
||||||
|
// Nach einem bestimmten, das es nicht gibt, ist dagegen 404 richtig.
|
||||||
|
const fremdesRepo = await request.post(`/api/projects/${projekt.id}/tasks/from-issues`, {
|
||||||
|
data: { repositoryId: "gibt-es-nicht" },
|
||||||
|
});
|
||||||
|
expect(fremdesRepo.status()).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Mehrere Repositories je Projekt", async ({ request, playwright }) => {
|
||||||
|
await login(request, ADMIN);
|
||||||
|
|
||||||
|
// Beim Anlegen setzen die Git-Felder das fuehrende Repository.
|
||||||
|
const projekt = (
|
||||||
|
await (
|
||||||
|
await request.post("/api/projects", {
|
||||||
|
data: { name: "Zwei Teile", repoUrl: "https://github.com/vercel/swr" },
|
||||||
|
})
|
||||||
|
).json()
|
||||||
|
).project;
|
||||||
|
|
||||||
|
expect(projekt.repositories).toHaveLength(1);
|
||||||
|
expect(projekt.repositories[0].repoOwner).toBe("vercel");
|
||||||
|
expect(projekt.repositories[0].repoName).toBe("swr");
|
||||||
|
|
||||||
|
// Ein zweites dazu.
|
||||||
|
const zweites = await request.post(`/api/projects/${projekt.id}/repositories`, {
|
||||||
|
data: { label: "Backend", repoUrl: "https://github.com/vercel/next.js" },
|
||||||
|
});
|
||||||
|
expect(zweites.status()).toBe(201);
|
||||||
|
const backendId = (await zweites.json()).repository.id;
|
||||||
|
|
||||||
|
// Dasselbe Repo zweimal ist immer ein Versehen.
|
||||||
|
const doppelt = await request.post(`/api/projects/${projekt.id}/repositories`, {
|
||||||
|
data: { repoUrl: "https://github.com/vercel/next.js" },
|
||||||
|
});
|
||||||
|
expect(doppelt.status()).toBe(409);
|
||||||
|
|
||||||
|
// Halbe Angaben ergeben kein Repository.
|
||||||
|
const leer = await request.post(`/api/projects/${projekt.id}/repositories`, {
|
||||||
|
data: { label: "Nur ein Name" },
|
||||||
|
});
|
||||||
|
expect(leer.status()).toBe(422);
|
||||||
|
|
||||||
|
const geladen = (await (await request.get(`/api/projects/${projekt.id}`)).json()).project;
|
||||||
|
expect(geladen.repositories).toHaveLength(2);
|
||||||
|
// Das fuehrende steht vorn.
|
||||||
|
expect(geladen.repositories[0].repoName).toBe("swr");
|
||||||
|
expect(geladen.repositories[1].label).toBe("Backend");
|
||||||
|
|
||||||
|
// Der Projekt-PATCH bearbeitet weiterhin nur das fuehrende.
|
||||||
|
await request.patch(`/api/projects/${projekt.id}`, { data: { autoSync: false } });
|
||||||
|
const danach = (await (await request.get(`/api/projects/${projekt.id}`)).json()).project;
|
||||||
|
expect(danach.repositories[0].autoSync).toBe(false);
|
||||||
|
expect(danach.repositories[1].autoSync).toBe(true);
|
||||||
|
|
||||||
|
// Fremde Konten kommen an kein Repository.
|
||||||
|
const fremd = await playwright.request.newContext({
|
||||||
|
baseURL: test.info().project.use.baseURL,
|
||||||
|
});
|
||||||
|
await login(fremd, ZWEITER);
|
||||||
|
expect((await fremd.get(`/api/projects/${projekt.id}/repositories`)).status()).toBe(404);
|
||||||
|
expect((await fremd.patch(`/api/repositories/${backendId}`, { data: {} })).status()).toBe(404);
|
||||||
|
expect((await fremd.delete(`/api/repositories/${backendId}`)).status()).toBe(404);
|
||||||
|
expect((await fremd.post(`/api/repositories/${backendId}/sync`)).status()).toBe(404);
|
||||||
|
await fremd.dispose();
|
||||||
|
|
||||||
|
// Entfernen laesst das andere stehen.
|
||||||
|
expect((await request.delete(`/api/repositories/${backendId}`)).ok()).toBeTruthy();
|
||||||
|
const zumSchluss = (await (await request.get(`/api/projects/${projekt.id}`)).json()).project;
|
||||||
|
expect(zumSchluss.repositories).toHaveLength(1);
|
||||||
|
expect(zumSchluss.repositories[0].repoName).toBe("swr");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async ({ request }) => {
|
test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async ({ request }) => {
|
||||||
@@ -218,6 +299,12 @@ test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async (
|
|||||||
const datei = JSON.parse(roh);
|
const datei = JSON.parse(roh);
|
||||||
const vorher = datei.projects.length;
|
const vorher = datei.projects.length;
|
||||||
|
|
||||||
|
// Version 2: die Repositories stehen als Liste in der Datei.
|
||||||
|
expect(datei.version).toBe(2);
|
||||||
|
const mitRepo = datei.projects.find((entry: { name: string }) => entry.name === "Zwei Teile");
|
||||||
|
expect(mitRepo.repositories).toHaveLength(1);
|
||||||
|
expect(mitRepo.repositories[0].repoName).toBe("swr");
|
||||||
|
|
||||||
// Ueberspringen aendert nichts.
|
// Ueberspringen aendert nichts.
|
||||||
const uebersprungen = await request.post("/api/projects/import", {
|
const uebersprungen = await request.post("/api/projects/import", {
|
||||||
data: { data: datei, strategy: "skip", dryRun: false },
|
data: { data: datei, strategy: "skip", dryRun: false },
|
||||||
@@ -239,3 +326,36 @@ test("Export enthaelt keine Geheimnisse, Import beachtet die Strategie", async (
|
|||||||
});
|
});
|
||||||
expect(muell.status()).toBe(422);
|
expect(muell.status()).toBe(422);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Dateien der Version 1 bleiben lesbar", async ({ request }) => {
|
||||||
|
await login(request, ADMIN);
|
||||||
|
|
||||||
|
// Damals stand genau ein Repository flach am Projekt. Wer eine solche
|
||||||
|
// Sicherung liegen hat, soll sie weiterhin einspielen koennen.
|
||||||
|
const alt = {
|
||||||
|
kind: "nebula.projects",
|
||||||
|
version: 1,
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "Aus Version eins",
|
||||||
|
repoUrl: "https://github.com/vercel/swr",
|
||||||
|
repoOwner: "vercel",
|
||||||
|
repoName: "swr",
|
||||||
|
autoSync: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const gelesen = await request.post("/api/projects/import", {
|
||||||
|
data: { data: alt, strategy: "rename", dryRun: false },
|
||||||
|
});
|
||||||
|
expect(gelesen.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const projekte = (await (await request.get("/api/projects")).json()).projects;
|
||||||
|
const uebernommen = projekte.find(
|
||||||
|
(entry: { name: string }) => entry.name === "Aus Version eins",
|
||||||
|
);
|
||||||
|
expect(uebernommen.repositories).toHaveLength(1);
|
||||||
|
expect(uebernommen.repositories[0].repoOwner).toBe("vercel");
|
||||||
|
expect(uebernommen.repositories[0].autoSync).toBe(false);
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nebula",
|
"name": "nebula",
|
||||||
"version": "0.9.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)",
|
"description": "Nebula – Dashboard fuer Vibe-Coding Projekte mit Git-Anbindung (GitHub, GitLab, Gitea)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user