package web_test import ( "context" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/netcell-it/deklarix/internal/rules" ) // checkAndReturnSubID führt eine Pre-Publish-Prüfung durch und liefert die // dabei angelegte submission_id — Hilfsfunktion für Tests, die einen // bereits existierenden Beitrag brauchen, ohne den ganzen Ablauf jedes Mal // auszuschreiben. func checkAndReturnSubID(t *testing.T, s interface { ServeHTTP(w http.ResponseWriter, r *http.Request) }, cookie *http.Cookie) string { t.Helper() form := checkForm() req := httptest.NewRequest(http.MethodPost, "/pruefen", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(cookie) w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("check status = %d, body: %s", w.Code, w.Body.String()) } body := w.Body.String() const marker = `name="submission_id" value="` idx := strings.Index(body, marker) if idx == -1 { t.Fatalf("expected a submission_id field in the result, got: %s", body) } rest := body[idx+len(marker):] return rest[:strings.Index(rest, `"`)] } func TestSubmissionListShowsOwnSubmissionsOnly(t *testing.T) { fs := newFakeStore() s := newServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}, fs) cookieA := seedAccount(t, fs, "Mandant A", "a@example.com") cookieB := seedAccount(t, fs, "Mandant B", "b@example.com") subA := checkAndReturnSubID(t, s, cookieA) _ = checkAndReturnSubID(t, s, cookieB) resp := getWithCookie(t, s, cookieA, "/beitraege") if resp.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body: %s", resp.Code, resp.Body.String()) } body := resp.Body.String() if !strings.Contains(body, "/beitraege/"+subA) { t.Errorf("expected Mandant A's own submission link, got: %s", body) } // Mandant B hat genau einen eigenen Beitrag, keinen von A. respB := getWithCookie(t, s, cookieB, "/beitraege") if strings.Contains(respB.Body.String(), "/beitraege/"+subA) { t.Errorf("Mandant B should not see Mandant A's submission, got: %s", respB.Body.String()) } } func TestSubmissionListRequiresSession(t *testing.T) { s, _, _ := newAuthedTestServer(t, fakeExtractor{}) req := httptest.NewRequest(http.MethodGet, "/beitraege", nil) w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303 redirect to /login", w.Code) } } func TestSubmissionDetailShowsFactsAndFindings(t *testing.T) { s, fs, cookie := newAuthedTestServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationPaid, DisclosurePresent: true, DisclosureWording: "Werbung", DisclosureBeforeCut: false, }}) subID := checkAndReturnSubID(t, s, cookie) _ = fs resp := getWithCookie(t, s, cookie, "/beitraege/"+subID) if resp.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body: %s", resp.Code, resp.Body.String()) } body := resp.Body.String() if !strings.Contains(body, "WK-004") { t.Errorf("expected the finding to be shown, got: %s", body) } if !strings.Contains(body, "/veroeffentlichen") { t.Errorf("expected an archive option for a checked submission, got: %s", body) } } func TestSubmissionDetailTenantIsolation(t *testing.T) { fs := newFakeStore() s := newServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}, fs) cookieA := seedAccount(t, fs, "Mandant A", "a@example.com") cookieB := seedAccount(t, fs, "Mandant B", "b@example.com") subA := checkAndReturnSubID(t, s, cookieA) resp := getWithCookie(t, s, cookieB, "/beitraege/"+subA) if resp.Code != http.StatusNotFound { t.Fatalf("cross-tenant detail status = %d, want 404", resp.Code) } } func TestParticipantAddUpdateDeleteFlow(t *testing.T) { s, _, cookie := newAuthedTestServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}) subID := checkAndReturnSubID(t, s, cookie) addResp := postForm(t, s, cookie, "/beitraege/"+subID+"/beteiligte", url.Values{ "role": {"creator"}, "name": {"Max Mustermann"}, }) if addResp.Code != http.StatusOK { t.Fatalf("add participant status = %d, body: %s", addResp.Code, addResp.Body.String()) } if !strings.Contains(addResp.Body.String(), "Max Mustermann") { t.Fatalf("expected the new participant in the response, got: %s", addResp.Body.String()) } detailResp := getWithCookie(t, s, cookie, "/beitraege/"+subID) if !strings.Contains(detailResp.Body.String(), "Max Mustermann") { t.Fatalf("expected the participant on the detail page, got: %s", detailResp.Body.String()) } // Beteiligten-ID aus dem Update-Formular extrahieren. body := addResp.Body.String() const marker = "/beteiligte/" idx := strings.Index(body, marker) if idx == -1 { t.Fatalf("expected a participant action URL, got: %s", body) } rest := body[idx+len(marker):] pID := rest[:strings.Index(rest, "/")] updateResp := postForm(t, s, cookie, "/beitraege/"+subID+"/beteiligte/"+pID+"/aktualisieren", url.Values{ "vorgegeben": {"on"}, "freigegeben": {"on"}, }) if updateResp.Code != http.StatusOK { t.Fatalf("update participant status = %d, body: %s", updateResp.Code, updateResp.Body.String()) } if !strings.Contains(updateResp.Body.String(), "freigegeben am") { t.Fatalf("expected an approval timestamp after freigegeben=true, got: %s", updateResp.Body.String()) } deleteResp := postForm(t, s, cookie, "/beitraege/"+subID+"/beteiligte/"+pID+"/loeschen", url.Values{}) if deleteResp.Code != http.StatusOK { t.Fatalf("delete participant status = %d, body: %s", deleteResp.Code, deleteResp.Body.String()) } if strings.Contains(deleteResp.Body.String(), "Max Mustermann") { t.Fatalf("expected the participant to be gone after delete, got: %s", deleteResp.Body.String()) } } func TestParticipantActionsRejectCrossTenantAccess(t *testing.T) { fs := newFakeStore() s := newServer(t, fakeExtractor{facts: rules.Facts{ Platform: "instagram", Jurisdiction: "DE", Consideration: rules.ConsiderationNone, }}, fs) cookieA := seedAccount(t, fs, "Mandant A", "a@example.com") cookieB := seedAccount(t, fs, "Mandant B", "b@example.com") subA := checkAndReturnSubID(t, s, cookieA) // Mandant B darf für As Beitrag gar keinen Beteiligten anlegen. addResp := postForm(t, s, cookieB, "/beitraege/"+subA+"/beteiligte", url.Values{ "role": {"creator"}, "name": {"Fremd"}, }) if addResp.Code != http.StatusNotFound { t.Fatalf("cross-tenant add status = %d, want 404", addResp.Code) } p, err := fs.CreateParticipant(context.Background(), subA, "creator", "Eigener Beteiligter", false, false) if err != nil { t.Fatalf("CreateParticipant: %v", err) } updateResp := postForm(t, s, cookieB, "/beitraege/"+subA+"/beteiligte/"+p.ID+"/aktualisieren", url.Values{ "vorgegeben": {"on"}, }) if updateResp.Code != http.StatusNotFound { t.Fatalf("cross-tenant update status = %d, want 404", updateResp.Code) } deleteResp := postForm(t, s, cookieB, "/beitraege/"+subA+"/beteiligte/"+p.ID+"/loeschen", url.Values{}) if deleteResp.Code != http.StatusNotFound { t.Fatalf("cross-tenant delete status = %d, want 404", deleteResp.Code) } if _, err := fs.GetParticipant(context.Background(), p.ID); err != nil { t.Fatalf("participant should still exist after rejected cross-tenant delete: %v", err) } }