// SPDX-License-Identifier: AGPL-3.0-or-later package web import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "bnema.dev/ermite/internal/domain" "bnema.dev/ermite/internal/ports" "bnema.dev/ermite/internal/ports/mocks" ) func issueRepoView(t testing.TB, mode domain.IssuesMode) ports.RepoView { t.Helper() v := publicRepo(t, "proj") v.Settings.Issues = mode return v } func testIssue(t testing.TB, n int64, state domain.IssueState, title, body string) domain.Issue { t.Helper() ti, err := domain.ParseIssueTitle(title) require.NoError(t, err) b, err := domain.ParseIssueBody(body) require.NoError(t, err) return domain.Issue{ Repo: repoName(t, "proj"), Number: n, Title: ti, Body: b, State: state, Author: "owner", CreatedAt: time.Now().Add(-time.Hour), UpdatedAt: time.Now().Add(-time.Minute), } } type issueRig struct { srv *Server issues *mocks.IssueService nums *mocks.IssueNumberResolver mrs *mocks.MergeRequestService } // newIssueRig serves proj with the given mode, signed in as the owner when // the cookie "tok" is sent. func newIssueRig(t *testing.T, mode domain.IssuesMode, withNumbers bool) *issueRig { t.Helper() r := &issueRig{issues: mocks.NewIssueService(t), mrs: mocks.NewMergeRequestService(t)} browse := mocks.NewBrowseService(t) browse.EXPECT().ListRefs(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(ports.RefPage{}, nil).Maybe() r.mrs.EXPECT().CountOpen(mock.Anything, mock.Anything, mock.Anything).Return(0, nil).Maybe() cfg := Config{ Repos: reposServing(t, issueRepoView(t, mode)), Browse: browse, MergeRequests: r.mrs, Issues: r.issues, Sessions: newSessions(t, "tok"), Limiter: &countingLimiter{}, } if withNumbers { r.nums = mocks.NewIssueNumberResolver(t) cfg.IssueNumbers = r.nums } r.srv = newTestServer(t, cfg) return r } func (r *issueRig) do(method, path string, owner bool, form url.Values) *httptest.ResponseRecorder { var body *strings.Reader if form != nil { body = strings.NewReader(form.Encode()) } else { body = strings.NewReader("") } req := httptest.NewRequest(method, path, body) if method == http.MethodPost { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", testPublicURL) } if owner { req.AddCookie(&http.Cookie{Name: CookieName, Value: "tok"}) } rec := httptest.NewRecorder() r.srv.ServeHTTP(rec, req) return rec } func TestIssueList(t *testing.T) { t.Parallel() r := newIssueRig(t, domain.IssuesOwner, false) r.issues.EXPECT().List(mock.Anything, mock.Anything, repoName(t, "proj"), ports.ListIssuesParams{State: domain.IssueStateOpen, PageSize: pageSize}). Return(ports.IssuePage{Issues: []domain.Issue{testIssue(t, 3, domain.IssueStateOpen, "crash on start", "")}, NextPageToken: "tok2"}, nil) r.issues.EXPECT().List(mock.Anything, mock.Anything, repoName(t, "proj"), ports.ListIssuesParams{State: domain.IssueStateClosed, PageSize: pageSize}). Return(ports.IssuePage{Issues: []domain.Issue{testIssue(t, 2, domain.IssueStateClosed, "old", "")}}, nil) rec := r.do("GET", "/proj/issues", false, nil) require.Equal(t, 200, rec.Code, rec.Body.String()) body := rec.Body.String() assert.Contains(t, body, `href="/proj/issues/3"`) assert.Contains(t, body, "crash <b>on</b> start") assert.Contains(t, body, `href="/proj/issues?page=tok2"`, "older page") assert.Contains(t, body, `href="/proj/issues?state=closed"`, "closed tab") assert.Contains(t, body, `href="/proj/issues" data-key="g i"`, "the Issues tab") assert.NotContains(t, body, "New issue", "no form for an anonymous visitor") assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control")) closed := r.do("GET", "/proj/issues?state=closed", false, nil) require.Equal(t, 200, closed.Code) assert.Contains(t, closed.Body.String(), `href="/proj/issues/2"`) assert.Equal(t, 404, r.do("GET", "/proj/issues?state=weird", false, nil).Code) owner := r.do("GET", "/proj/issues", true, nil) require.Equal(t, 200, owner.Code) assert.Contains(t, owner.Body.String(), `href="/proj/issues/new"`) assert.Equal(t, "private, no-store", owner.Header().Get("Cache-Control")) } func TestIssueShow(t *testing.T) { t.Parallel() r := newIssueRig(t, domain.IssuesPublic, false) c := domain.IssueComment{ID: 1, Author: "owner", CreatedAt: time.Now()} c.Text, _ = domain.ParseIssueBody("see https://example.org ") r.issues.EXPECT().Get(mock.Anything, mock.Anything, repoName(t, "proj"), int64(5)).Return(ports.IssueDetail{ Issue: testIssue(t, 5, domain.IssueStateOpen, "bug", "# Title\n\n**bold**\n\n"), Comments: []domain.IssueComment{c}, }, nil) anon := r.do("GET", "/proj/issues/5", false, nil) require.Equal(t, 200, anon.Code, anon.Body.String()) body := anon.Body.String() assert.Contains(t, body, "bold", "Markdown body") assert.NotContains(t, body, "