package web_test import ( "bytes" "context" "mime/multipart" "net/http" "net/http/httptest" "net/url" "testing" "github.com/netcell-it/deklarix/internal/rules" "github.com/netcell-it/deklarix/internal/web" ) // tinyPNG ist das kleinstmögliche gültige PNG (1x1 transparent) — genug, // um einen echten Datei-Upload zu simulieren, ohne eine Bilddatei aus // dem Repo laden zu müssen. var tinyPNG = []byte{ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, } // postCheckWithImage stellt eine echte multipart/form-data-Anfrage wie // der Browser sie schickt (im Gegensatz zu postForm, das urlencoded // postet) — checkForm()-Felder plus ein optionales "standbild". func postCheckWithImage(t *testing.T, s *web.Server, cookie *http.Cookie, imageBytes []byte, contentType string) *httptest.ResponseRecorder { t.Helper() var buf bytes.Buffer mw := multipart.NewWriter(&buf) for key, val := range checkForm() { if err := mw.WriteField(key, val[0]); err != nil { t.Fatalf("WriteField(%s): %v", key, err) } } if imageBytes != nil { part, err := mw.CreatePart(map[string][]string{ "Content-Disposition": {`form-data; name="standbild"; filename="screenshot.png"`}, "Content-Type": {contentType}, }) if err != nil { t.Fatalf("CreatePart: %v", err) } if _, err := part.Write(imageBytes); err != nil { t.Fatalf("Write image bytes: %v", err) } } if err := mw.Close(); err != nil { t.Fatalf("multipart Close: %v", err) } req := httptest.NewRequest(http.MethodPost, "/pruefen", &buf) req.Header.Set("Content-Type", mw.FormDataContentType()) if cookie != nil { req.AddCookie(cookie) } w := httptest.NewRecorder() s.ServeHTTP(w, req) return w } func TestCheckWithImageUploadStoresAsset(t *testing.T) { s, fs, cookie := newAuthedTestServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}) resp := postCheckWithImage(t, s, cookie, tinyPNG, "image/png") if resp.Code != http.StatusOK { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } var subID string for id := range fs.submissions { subID = id } if subID == "" { t.Fatal("expected a submission to have been created") } asset, err := fs.GetLatestAssetForSubmission(context.Background(), subID) if err != nil { t.Fatalf("expected an asset to be stored, got err: %v", err) } if asset.Kind != "image" || asset.SHA256 == "" { t.Errorf("unexpected asset: %+v", asset) } } func TestCheckWithoutImageStoresNoAsset(t *testing.T) { s, fs, cookie := newAuthedTestServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}) resp := postForm(t, s, cookie, "/pruefen", checkForm()) if resp.Code != http.StatusOK { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } var subID string for id := range fs.submissions { subID = id } if _, err := fs.GetLatestAssetForSubmission(context.Background(), subID); err == nil { t.Fatal("expected no asset when none was uploaded") } } func TestCheckRejectsNonImageUpload(t *testing.T) { s, fs, cookie := newAuthedTestServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}) resp := postCheckWithImage(t, s, cookie, []byte("kein bild, nur text"), "text/plain") if resp.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400 for a non-image upload, body: %s", resp.Code, resp.Body.String()) } if len(fs.submissions) != 0 { t.Error("expected no submission to be created when the upload is rejected") } } func TestCheckRejectsOversizedUpload(t *testing.T) { s, _, cookie := newAuthedTestServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}) tooLarge := bytes.Repeat([]byte{0xff}, 9<<20) // 9 MiB > 8 MiB Limit resp := postCheckWithImage(t, s, cookie, tooLarge, "image/png") if resp.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400 for an oversized upload, body: %s", resp.Code, resp.Body.String()) } } // TestArchiveMetadataHashDiffersWhenAssetPresent prüft schwarz-verpackt // (ohne PDF-Interna zu kennen — der Dossier-Content wird komprimiert, // ein hex-Hash taucht daher nicht als durchsuchbarer String in den // PDF-Rohbytes auf, siehe internal/dossier/content_test.go für die // Prüfung auf Ebene der PDF-Inhaltsstruktur), dass ein hochgeladenes // Standbild tatsächlich in den archivierten Metadaten-Hash einfließt: // zwei sonst identische Beiträge, einer mit, einer ohne Bild, müssen // unterschiedliche evidence_package.SHA256 ergeben. func TestArchiveMetadataHashDiffersWhenAssetPresent(t *testing.T) { fakeEx := fakeExtractor{ facts: rules.Facts{Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone}, raw: []byte(`{"gegenleistung":"keine","kennzeichnung_vorhanden":false,"kennzeichnung_wortlaut":"","kennzeichnung_vor_kuerzung":false}`), } s, fs, cookie := newAuthedTestServer(t, fakeEx) withImageResp := postCheckWithImage(t, s, cookie, tinyPNG, "image/png") if withImageResp.Code != http.StatusOK { t.Fatalf("check (mit Bild) status = %d, body: %s", withImageResp.Code, withImageResp.Body.String()) } var withImageSubID string for id := range fs.submissions { withImageSubID = id } archiveWithImage := postForm(t, s, cookie, "/veroeffentlichen", url.Values{"submission_id": {withImageSubID}}) if archiveWithImage.Code != http.StatusOK { t.Fatalf("archive (mit Bild) status = %d, body: %s", archiveWithImage.Code, archiveWithImage.Body.String()) } pkgWithImage, err := fs.GetLatestEvidencePackage(context.Background(), withImageSubID) if err != nil { t.Fatalf("GetLatestEvidencePackage (mit Bild): %v", err) } withoutImageResp := postForm(t, s, cookie, "/pruefen", checkForm()) if withoutImageResp.Code != http.StatusOK { t.Fatalf("check (ohne Bild) status = %d, body: %s", withoutImageResp.Code, withoutImageResp.Body.String()) } var withoutImageSubID string for id := range fs.submissions { if id != withImageSubID { withoutImageSubID = id } } archiveWithoutImage := postForm(t, s, cookie, "/veroeffentlichen", url.Values{"submission_id": {withoutImageSubID}}) if archiveWithoutImage.Code != http.StatusOK { t.Fatalf("archive (ohne Bild) status = %d, body: %s", archiveWithoutImage.Code, archiveWithoutImage.Body.String()) } pkgWithoutImage, err := fs.GetLatestEvidencePackage(context.Background(), withoutImageSubID) if err != nil { t.Fatalf("GetLatestEvidencePackage (ohne Bild): %v", err) } if pkgWithImage.SHA256 == pkgWithoutImage.SHA256 { t.Fatal("expected different metadata hashes for an archived submission with vs. without an uploaded asset") } }