feat: CSV-Import für Mitarbeiter
Schließt den zweiten Punkt der geplanten Onboarding-Reihenfolge. Admin lädt eine CSV mit Spalten email (Pflicht) und role (optional, Standard mitarbeiter) unter GET /verwaltung/nutzer/import hoch. Wie beim einzelnen Nutzer-Anlegen gibt es keine Einladungsmail — pro Zeile wird ein Einmalpasswort erzeugt und nur auf der Ergebnisseite direkt nach dem Import angezeigt, danach nirgendwo mehr abrufbar. Kein Alles-oder-nichts-Import: eine ungültige Zeile blockiert nicht die übrigen, jede Zeile bekommt ihr eigenes Ergebnis. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
149
internal/web/csv_import_handlers_test.go
Normal file
149
internal/web/csv_import_handlers_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/netcell-it/deklarix/internal/web"
|
||||
)
|
||||
|
||||
// postCSV lädt csvContent als multipart-Formular mit Feldname "csv" hoch
|
||||
// — reale Browser-Uploads laufen genauso über multipart/form-data.
|
||||
func postCSV(t *testing.T, s *web.Server, cookie *http.Cookie, path, csvContent string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
mw := multipart.NewWriter(&body)
|
||||
fw, err := mw.CreateFormFile("csv", "mitarbeiter.csv")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFormFile: %v", err)
|
||||
}
|
||||
if _, err := fw.Write([]byte(csvContent)); err != nil {
|
||||
t.Fatalf("Write: %v", err)
|
||||
}
|
||||
if err := mw.Close(); err != nil {
|
||||
t.Fatalf("mw.Close: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, path, &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
if cookie != nil {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
s.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestAdminCanImportMitarbeiterPerCSV(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
adminCookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin")
|
||||
|
||||
resp := postCSV(t, s, adminCookie, "/verwaltung/nutzer/import", "email,role\nlisa@example.com,verantwortlicher\ntom@example.com,\n")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
body := resp.Body.String()
|
||||
if !strings.Contains(body, "lisa@example.com") || !strings.Contains(body, "tom@example.com") {
|
||||
t.Fatalf("expected both imported emails in the result, got: %s", body)
|
||||
}
|
||||
|
||||
lisa, err := fs.GetUserByEmail(context.Background(), "lisa@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail (lisa): %v", err)
|
||||
}
|
||||
if lisa.Role != "verantwortlicher" {
|
||||
t.Errorf("lisa.Role = %q, want verantwortlicher", lisa.Role)
|
||||
}
|
||||
tom, err := fs.GetUserByEmail(context.Background(), "tom@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserByEmail (tom): %v", err)
|
||||
}
|
||||
if tom.Role != "mitarbeiter" {
|
||||
t.Errorf("tom.Role = %q, want default mitarbeiter", tom.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVImportZeigtEinmalpasswortDasFunktioniert(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
adminCookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin")
|
||||
|
||||
resp := postCSV(t, s, adminCookie, "/verwaltung/nutzer/import", "email\nneu@example.com\n")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
|
||||
// Das angezeigte Einmalpasswort aus der <code>-Zelle extrahieren.
|
||||
body := resp.Body.String()
|
||||
start := strings.Index(body, "<code>")
|
||||
end := strings.Index(body, "</code>")
|
||||
if start == -1 || end == -1 {
|
||||
t.Fatalf("expected a <code>-Zelle mit dem Einmalpasswort, got: %s", body)
|
||||
}
|
||||
passwort := body[start+len("<code>") : end]
|
||||
|
||||
loginResp := postForm(t, s, nil, "/login", map[string][]string{
|
||||
"email": {"neu@example.com"}, "password": {passwort},
|
||||
})
|
||||
if loginResp.Code != http.StatusSeeOther {
|
||||
t.Fatalf("login with the generated password: status = %d, body: %s", loginResp.Code, loginResp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVImportMeldetUngueltigeRolleOhneDieAnderenZuBlockieren(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
adminCookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin")
|
||||
|
||||
resp := postCSV(t, s, adminCookie, "/verwaltung/nutzer/import", "email,role\nboese@example.com,betreiber\ngut@example.com,mitarbeiter\n")
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
|
||||
}
|
||||
if _, err := fs.GetUserByEmail(context.Background(), "boese@example.com"); err == nil {
|
||||
t.Error("expected no user to be created for an invalid role (betreiber)")
|
||||
}
|
||||
if _, err := fs.GetUserByEmail(context.Background(), "gut@example.com"); err != nil {
|
||||
t.Error("expected the valid row to still be imported despite the earlier invalid row")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVImportOhneDateiZeigtFehler(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
adminCookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin")
|
||||
|
||||
var body bytes.Buffer
|
||||
mw := multipart.NewWriter(&body)
|
||||
if err := mw.Close(); err != nil {
|
||||
t.Fatalf("mw.Close: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/verwaltung/nutzer/import", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.AddCookie(adminCookie)
|
||||
w := httptest.NewRecorder()
|
||||
s.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "CSV-Datei auswählen") {
|
||||
t.Errorf("expected an error asking for a file, got: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMitarbeiterCannotAccessCSVImport(t *testing.T) {
|
||||
fs := newFakeStore()
|
||||
s := newServer(t, fs)
|
||||
cookie := seedAccountWithRole(t, fs, "Test-Mandant", "mitarbeiter@example.com", "mitarbeiter")
|
||||
|
||||
resp := getWithCookie(t, s, cookie, "/verwaltung/nutzer/import")
|
||||
if resp.Code != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404 for role mitarbeiter", resp.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user