detached 5f5b000

inbox_moderation_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"net/http"
7	"net/http/httptest"
8	"strings"
9	"testing"
10	"time"
11
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/mock"
14	"github.com/stretchr/testify/require"
15
16	"bnema.dev/ermite/internal/domain"
17	"bnema.dev/ermite/internal/ports"
18	"bnema.dev/ermite/internal/ports/mocks"
19)
20
21const testPendingID = "aaaaaaaaaaaaaaaaaaaaaaaaaa"
22
23func testPendingIssue(t testing.TB) domain.PendingIssue {
24	t.Helper()
25	id, err := domain.NewPendingIssueID()
26	require.NoError(t, err)
27	title, err := domain.ParseIssueTitle("<script>alert(1)</script> crash")
28	require.NoError(t, err)
29	body, err := domain.ParseIssueBody("**not bold** [x](javascript:alert(1)) <img src=x onerror=y>\n" + strings.Repeat("long line ", 60))
30	require.NoError(t, err)
31	return domain.PendingIssue{ID: id, Repo: repoName(t, "alpha"), Title: title, Body: body, Author: "Eve <i>", CreatedAt: time.Now().Add(-3 * time.Hour)}
32}
33
34func moderationServer(t *testing.T, inbox ports.InboxService, mod ports.IssueModerationService) *Server {
35	t.Helper()
36	return newTestServer(t, Config{
37		Repos: reposServing(t, publicRepo(t, "alpha")), Browse: homeBrowse(t), Inbox: inbox, Moderation: mod,
38		Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{},
39	})
40}
41
42func TestInboxShowsPendingIssuesAsPlainTextFirst(t *testing.T) {
43	t.Parallel()
44	p := testPendingIssue(t)
45	ov := inboxOverview(t)
46	ov.Pending = []domain.PendingIssue{p, {ID: p.ID, Repo: repoName(t, "beta"), Title: p.Title, CreatedAt: time.Now()}}
47	ov.Counts.Pending = 2
48	svc := mocks.NewInboxService(t)
49	svc.EXPECT().Overview(mock.Anything, mock.Anything, domain.RepoName{}).Return(ov, nil)
50	srv := moderationServer(t, svc, mocks.NewIssueModerationService(t))
51
52	rec := inboxGet(srv, "/-/inbox", true)
53	require.Equal(t, 200, rec.Code)
54	body := rec.Body.String()
55	assert.Contains(t, body, "<strong>2</strong> awaiting moderation")
56	assert.Less(t, strings.Index(body, `id="inbox-pending"`), strings.Index(body, `id="inbox-mrs"`), "first section")
57	assert.Contains(t, body, "&lt;script&gt;alert(1)&lt;/script&gt; crash")
58	assert.NotContains(t, body, "<script>alert")
59	assert.Contains(t, body, "**not bold**", "no Markdown")
60	assert.NotContains(t, body, "<strong>not bold")
61	assert.NotContains(t, body, `href="javascript`)
62	assert.NotContains(t, body, "<img")
63	assert.Contains(t, body, "Eve &lt;i&gt;")
64	assert.Contains(t, body, ">anonymous<", "no label")
65	assert.Contains(t, body, "…</p>", "a preview of about 300 characters")
66	assert.Contains(t, body, `action="/-/inbox/pending/alpha/`+p.ID.String()+`/approve"`)
67	assert.Contains(t, body, `/reject"`)
68	assert.Contains(t, body, `/ban"`)
69	assert.Contains(t, body, "Reject and ban")
70}
71
72func TestInboxPendingKeepsTheFilterInItsActions(t *testing.T) {
73	t.Parallel()
74	p := testPendingIssue(t)
75	ov := inboxOverview(t)
76	ov.Pending = []domain.PendingIssue{p}
77	ov.Counts.Pending = 1
78	svc := mocks.NewInboxService(t)
79	svc.EXPECT().Overview(mock.Anything, mock.Anything, repoName(t, "alpha")).Return(ov, nil)
80	srv := moderationServer(t, svc, mocks.NewIssueModerationService(t))
81	rec := inboxGet(srv, "/-/inbox?repo=alpha", true)
82	assert.Contains(t, rec.Body.String(), `/approve?repo=alpha"`)
83}
84
85func TestInboxModerationActions(t *testing.T) {
86	t.Parallel()
87	post := func(srv *Server, path string, hdr map[string]string) *httptest.ResponseRecorder {
88		req := httptest.NewRequest(http.MethodPost, path, nil)
89		for k, v := range hdr {
90			req.Header.Set(k, v)
91		}
92		req.AddCookie(&http.Cookie{Name: CookieName, Value: "tok"})
93		rec := httptest.NewRecorder()
94		srv.ServeHTTP(rec, req)
95		return rec
96	}
97	id, err := domain.ParsePendingIssueID(testPendingID)
98	require.NoError(t, err)
99	alpha := repoName(t, "alpha")
100	site := map[string]string{"Origin": testPublicURL}
101
102	t.Run("approve", func(t *testing.T) {
103		t.Parallel()
104		mod := mocks.NewIssueModerationService(t)
105		mod.EXPECT().Approve(mock.Anything, mock.Anything, alpha, id).Return(domain.Issue{}, nil).Once()
106		rec := post(moderationServer(t, mocks.NewInboxService(t), mod), "/-/inbox/pending/alpha/"+testPendingID+"/approve?repo=alpha", site)
107		assert.Equal(t, 303, rec.Code)
108		assert.Equal(t, "/-/inbox?repo=alpha", rec.Header().Get("Location"))
109		assert.Equal(t, "no-store", rec.Header().Get("Cache-Control"))
110	})
111	t.Run("reject", func(t *testing.T) {
112		t.Parallel()
113		mod := mocks.NewIssueModerationService(t)
114		mod.EXPECT().Reject(mock.Anything, mock.Anything, alpha, id, false).Return(nil).Once()
115		rec := post(moderationServer(t, mocks.NewInboxService(t), mod), "/-/inbox/pending/alpha/"+testPendingID+"/reject", site)
116		assert.Equal(t, 303, rec.Code)
117		assert.Equal(t, "/-/inbox", rec.Header().Get("Location"))
118	})
119	t.Run("ban", func(t *testing.T) {
120		t.Parallel()
121		mod := mocks.NewIssueModerationService(t)
122		mod.EXPECT().Reject(mock.Anything, mock.Anything, alpha, id, true).Return(nil).Once()
123		rec := post(moderationServer(t, mocks.NewInboxService(t), mod), "/-/inbox/pending/alpha/"+testPendingID+"/ban", site)
124		assert.Equal(t, 303, rec.Code)
125	})
126	t.Run("gone", func(t *testing.T) {
127		t.Parallel()
128		mod := mocks.NewIssueModerationService(t)
129		mod.EXPECT().Approve(mock.Anything, mock.Anything, alpha, id).Return(domain.Issue{}, domain.ErrNotFound).Once()
130		rec := post(moderationServer(t, mocks.NewInboxService(t), mod), "/-/inbox/pending/alpha/"+testPendingID+"/approve", site)
131		assert.Equal(t, 404, rec.Code)
132	})
133	t.Run("a malformed id is not found and calls nothing", func(t *testing.T) {
134		t.Parallel()
135		rec := post(moderationServer(t, mocks.NewInboxService(t), mocks.NewIssueModerationService(t)), "/-/inbox/pending/alpha/nope/approve", site)
136		assert.Equal(t, 404, rec.Code)
137	})
138	t.Run("the gate: no origin, no session", func(t *testing.T) {
139		t.Parallel()
140		srv := moderationServer(t, mocks.NewInboxService(t), mocks.NewIssueModerationService(t))
141		assert.Equal(t, 403, post(srv, "/-/inbox/pending/alpha/"+testPendingID+"/approve", nil).Code)
142		req := httptest.NewRequest(http.MethodPost, "/-/inbox/pending/alpha/"+testPendingID+"/approve", nil)
143		req.Header.Set("Origin", testPublicURL)
144		rec := httptest.NewRecorder()
145		srv.ServeHTTP(rec, req)
146		assert.Equal(t, 403, rec.Code, "anonymous")
147	})
148}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 45

↑↓ moveenter openesc close