package web import ( "archive/zip" "bytes" "context" "io" "net/http" "net/http/httptest" "strings" "testing" "munin/internal/auth" "munin/internal/store" ) // Tests für den Download als ZIP (handlers_download.go). // download ruft einen Download ab und gibt die Antwort zurück. func download(h http.Handler, filePath string, cookie *http.Cookie) *httptest.ResponseRecorder { r := httptest.NewRequest(http.MethodGet, filePath, nil) r.AddCookie(cookie) w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } // zipContents öffnet ein heruntergeladenes ZIP und gibt Pfad → Inhalt zurück. func zipContents(t *testing.T, data []byte) map[string]string { t.Helper() archive, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { t.Fatalf("kein gültiges ZIP: %v", err) } content := make(map[string]string) for _, f := range archive.File { rc, err := f.Open() if err != nil { t.Fatal(err) } b, _ := io.ReadAll(rc) rc.Close() content[f.Name] = string(b) } return content } func TestProjectDownloadAsAdmin(t *testing.T) { srv, db, project := testServer(t) h := testHandler(t, srv) ctx := context.Background() if _, err := db.PutEntry(ctx, store.EntryWrite{ ProjectID: project.ID, Slug: "deployment", Title: "Deployment", Content: "Wir deployen freitags nie.", Source: store.SourceHuman, }); err != nil { t.Fatal(err) } cookie := login(t, h, createKey(t, srv, db, project.ID, "verwalterin", auth.RoleAdmin)) w := download(h, "/p/testprojekt/export.zip", cookie) if w.Code != http.StatusOK { t.Fatalf("Status %d, erwartet 200: %s", w.Code, w.Body.String()) } if ct := w.Header().Get("Content-Type"); ct != "application/zip" { t.Errorf("Content-Type %q", ct) } if cd := w.Header().Get("Content-Disposition"); !strings.Contains(cd, `filename="munin-testprojekt-`) { t.Errorf("Content-Disposition %q", cd) } content := zipContents(t, w.Body.Bytes()) if !strings.Contains(content["testprojekt/deployment.md"], "freitags nie") { t.Errorf("Eintrag fehlt im ZIP, enthalten: %v", keysOf(content)) } if _, ok := content["testprojekt/_index.md"]; !ok { t.Error("_index.md fehlt im ZIP") } // Der Download steht im Protokoll des Projekts. events, _ := db.ListAudit(ctx, store.AuditFilter{ProjectID: project.ID}) found := false for _, e := range events { if e.Action == store.ActionExportDownload && e.Actor == "verwalterin" { found = true } } if !found { t.Error("Download fehlt im Protokoll") } } // TestProjectDownloadOnlyForAdmins: Lesen und Schreiben reichen (vorerst) // nicht, siehe Session.CanDownloadProject. func TestProjectDownloadOnlyForAdmins(t *testing.T) { srv, db, project := testServer(t) h := testHandler(t, srv) for _, role := range []auth.Role{auth.RoleRead, auth.RoleWrite} { cookie := login(t, h, createKey(t, srv, db, project.ID, "key-"+string(role), role)) if w := download(h, "/p/testprojekt/export.zip", cookie); w.Code != http.StatusForbidden { t.Errorf("Rolle %s: Status %d, erwartet 403", role, w.Code) } } } // TestProjectDownloadForeignProject: Der Admin von Projekt A kommt nicht an // das ZIP von Projekt B. func TestProjectDownloadForeignProject(t *testing.T) { srv, db, project := testServer(t) h := testHandler(t, srv) if _, err := db.CreateProject(context.Background(), project.WorkspaceID, "anderes", "Anderes", ""); err != nil { t.Fatal(err) } cookie := login(t, h, createKey(t, srv, db, project.ID, "verwalterin", auth.RoleAdmin)) if w := download(h, "/p/anderes/export.zip", cookie); w.Code == http.StatusOK { t.Error("ZIP eines fremden Projekts ausgeliefert") } } func TestWorkspaceDownload(t *testing.T) { srv, db, project := testServer(t) h := testHandler(t, srv) ctx := context.Background() other, err := db.CreateProject(ctx, project.WorkspaceID, "anderes", "Anderes", "") if err != nil { t.Fatal(err) } for _, p := range []store.Project{project, other} { if _, err := db.PutEntry(ctx, store.EntryWrite{ ProjectID: p.ID, Slug: "notiz", Title: "Notiz", Content: "aus " + p.Slug, Source: store.SourceHuman, }); err != nil { t.Fatal(err) } } wsAdmin := loginAsWorkspaceAdmin(t, srv, h, project.WorkspaceID) platform := loginAsPlatformAdmin(t, h, project.WorkspaceID) for name, cookie := range map[string]*http.Cookie{"Workspace-Admin": wsAdmin.cookie, "Plattform-Admin": platform} { w := download(h, "/admin/export.zip", cookie) if w.Code != http.StatusOK { t.Fatalf("%s: Status %d, erwartet 200", name, w.Code) } content := zipContents(t, w.Body.Bytes()) for _, filePath := range []string{"_workspace.md", "testprojekt/notiz.md", "anderes/notiz.md"} { if _, ok := content[filePath]; !ok { t.Errorf("%s: %s fehlt im ZIP, enthalten: %v", name, filePath, keysOf(content)) } } } // Ein Projekt-Admin verwaltet keine Projekte, also auch kein Workspace-ZIP. key := login(t, h, createKey(t, srv, db, project.ID, "verwalterin", auth.RoleAdmin)) if w := download(h, "/admin/export.zip", key); w.Code != http.StatusForbidden { t.Errorf("Projekt-Admin: Status %d, erwartet 403", w.Code) } } func TestFilenamePart(t *testing.T) { cases := map[string]string{ "Plattform": "plattform", "Büro Süd & Nord": "buero-sued-nord", `Böse "Name"` + "\r\n": "boese-name", "!!!": "workspace", "": "workspace", } for input, want := range cases { if got := filenamePart(input, "workspace"); got != want { t.Errorf("dateinamenTeil(%q) = %q, erwartet %q", input, got, want) } } } func keysOf(m map[string]string) []string { var list []string for k := range m { list = append(list, k) } return list }