// 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="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, "