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, "<script>alert(1)</script> 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 <i>")
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}