munin/internal/web/download_test.go
2026-10-19 10:13:56 +02:00

186 lines
5.6 KiB
Go

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
}