194 lines
6 KiB
Go
194 lines
6 KiB
Go
package web
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"munin/internal/auth"
|
|
"munin/internal/store"
|
|
)
|
|
|
|
// importRequest baut einen Upload wie das Formular der Import-Seite.
|
|
func importRequest(t *testing.T, cookie *http.Cookie, csrf string, files map[string]string, fields map[string]string) *http.Request {
|
|
t.Helper()
|
|
|
|
var archive bytes.Buffer
|
|
zw := zip.NewWriter(&archive)
|
|
for filePath, content := range files {
|
|
f, _ := zw.Create(filePath)
|
|
f.Write([]byte(content))
|
|
}
|
|
zw.Close()
|
|
|
|
var body bytes.Buffer
|
|
mw := multipart.NewWriter(&body)
|
|
mw.WriteField(auth.CSRFFieldName, csrf)
|
|
for name, value := range fields {
|
|
mw.WriteField(name, value)
|
|
}
|
|
part, _ := mw.CreateFormFile("archiv", "import.zip")
|
|
part.Write(archive.Bytes())
|
|
mw.Close()
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/p/testprojekt/import", &body)
|
|
r.Header.Set("Content-Type", mw.FormDataContentType())
|
|
r.AddCookie(cookie)
|
|
return r
|
|
}
|
|
|
|
// adminWithToken meldet einen Admin-Key an und berechnet sein CSRF-Token so,
|
|
// wie es das Template tut.
|
|
func adminWithToken(t *testing.T, srv *Server, db *store.Store, project store.Project, h http.Handler) (*http.Cookie, string) {
|
|
t.Helper()
|
|
key := createKey(t, srv, db, project.ID, "verwalterin", auth.RoleAdmin)
|
|
cookie := login(t, h, key)
|
|
|
|
keys, _ := db.ListAPIKeys(context.Background(), project.ID)
|
|
subject := auth.SubjectKeyPrefix + project.WorkspaceID + ":" + keys[0].ID
|
|
return cookie, auth.NewSigner([]byte(testSecret)).CSRFToken(subject)
|
|
}
|
|
|
|
func TestImportPreviewThenApply(t *testing.T) {
|
|
srv, db, project := testServer(t)
|
|
h := testHandler(t, srv)
|
|
cookie, csrf := adminWithToken(t, srv, db, project, h)
|
|
ctx := context.Background()
|
|
|
|
// Suchindex vorher aufbauen. Der Import muss ihn nachziehen, sonst fände
|
|
// die Suche die neuen Einträge erst nach einem Neustart.
|
|
search := httptest.NewRequest(http.MethodGet, "/p/testprojekt/?q=zebrastreifen", nil)
|
|
search.AddCookie(cookie)
|
|
h.ServeHTTP(httptest.NewRecorder(), search)
|
|
|
|
files := map[string]string{
|
|
"doku/verkehr.md": "# Verkehr\n\nAm Zebrastreifen anhalten.",
|
|
"doku/_index.md": "Übersicht",
|
|
}
|
|
|
|
// Vorschau: nichts geschrieben.
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, importRequest(t, cookie, csrf, files, map[string]string{"vorschau": "1", "modus": "skip"}))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Vorschau: Status %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if !strings.Contains(w.Body.String(), "Vorschau") || !strings.Contains(w.Body.String(), "verkehr") {
|
|
t.Error("die Vorschau zeigt die Datei nicht")
|
|
}
|
|
if _, err := db.EntryBySlug(ctx, project.ID, "verkehr"); err == nil {
|
|
t.Fatal("die Vorschau hat geschrieben")
|
|
}
|
|
|
|
// Übernehmen.
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, importRequest(t, cookie, csrf, files, map[string]string{"modus": "skip"}))
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("Import: Status %d: %s", w.Code, w.Body.String())
|
|
}
|
|
e, err := db.EntryBySlug(ctx, project.ID, "verkehr")
|
|
if err != nil {
|
|
t.Fatalf("Eintrag fehlt nach dem Import: %v", err)
|
|
}
|
|
if e.Author != "verwalterin" || e.Title != "Verkehr" {
|
|
t.Errorf("Autor %q, Titel %q", e.Author, e.Title)
|
|
}
|
|
|
|
search = httptest.NewRequest(http.MethodGet, "/p/testprojekt/?q=zebrastreifen", nil)
|
|
search.AddCookie(cookie)
|
|
w = httptest.NewRecorder()
|
|
h.ServeHTTP(w, search)
|
|
if !strings.Contains(w.Body.String(), "/e/verkehr") {
|
|
t.Error("die Suche findet den importierten Eintrag nicht — Suchindex nicht nachgezogen")
|
|
}
|
|
|
|
// Genau ein Protokolleintrag für den ganzen Import.
|
|
events, err := db.ListAudit(ctx, store.AuditFilter{ProjectID: project.ID, Limit: 50})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
count := 0
|
|
for _, ev := range events {
|
|
if ev.Action == store.ActionEntryImport {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("%d Import-Einträge im Protokoll, erwartet 1", count)
|
|
}
|
|
}
|
|
|
|
func TestImportNeedsAdmin(t *testing.T) {
|
|
srv, db, project := testServer(t)
|
|
h := testHandler(t, srv)
|
|
|
|
key := createKey(t, srv, db, project.ID, "schreiber", auth.RoleWrite)
|
|
cookie := login(t, h, key)
|
|
|
|
r := httptest.NewRequest(http.MethodGet, "/p/testprojekt/import", nil)
|
|
r.AddCookie(cookie)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("Status = %d, erwartet %d", w.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
func TestImportRejectsNonZip(t *testing.T) {
|
|
srv, db, project := testServer(t)
|
|
h := testHandler(t, srv)
|
|
cookie, csrf := adminWithToken(t, srv, db, project, h)
|
|
|
|
var body bytes.Buffer
|
|
mw := multipart.NewWriter(&body)
|
|
mw.WriteField(auth.CSRFFieldName, csrf)
|
|
part, _ := mw.CreateFormFile("archiv", "kaputt.zip")
|
|
part.Write([]byte("das ist kein zip"))
|
|
mw.Close()
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "/p/testprojekt/import", &body)
|
|
r.Header.Set("Content-Type", mw.FormDataContentType())
|
|
r.AddCookie(cookie)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
|
|
if w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "ZIP") {
|
|
t.Errorf("Status %d, Antwort ohne verständliche Meldung", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestLimitBodiesPerRoute prüft, dass /mcp und der Import ihre eigene,
|
|
// größere Grenze bekommen und alles andere die kleine. Früher lag ein
|
|
// einziges Limit über allem und schnitt MCP auf 1 MB ab.
|
|
func TestLimitBodiesPerRoute(t *testing.T) {
|
|
var read int64
|
|
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
read, _ = io.Copy(io.Discard, r.Body)
|
|
})
|
|
h := LimitBodies(inner, BodyLimits{Web: 100, API: 1000, Import: 5000})
|
|
|
|
cases := []struct {
|
|
method, filePath string
|
|
want int64
|
|
}{
|
|
{http.MethodPost, "/p/x/save", 100},
|
|
{http.MethodPost, "/mcp", 1000},
|
|
{http.MethodPost, "/p/x/import", 5000},
|
|
{http.MethodGet, "/p/x/import", 100},
|
|
{http.MethodPost, "/p/x/e/import", 100}, // ein Eintrag namens "import", nicht die Import-Seite
|
|
}
|
|
for _, f := range cases {
|
|
read = 0
|
|
h.ServeHTTP(httptest.NewRecorder(),
|
|
httptest.NewRequest(f.method, f.filePath, strings.NewReader(strings.Repeat("x", 10000))))
|
|
if read != f.want {
|
|
t.Errorf("%s %s: %d Bytes gelesen, erwartet %d", f.method, f.filePath, read, f.want)
|
|
}
|
|
}
|
|
}
|