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>
150 lines
5.0 KiB
Go
150 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|