1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "context"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16
17 "bnema.dev/ermite/internal/domain"
18 "bnema.dev/ermite/internal/ports"
19 "bnema.dev/ermite/internal/ports/mocks"
20)
21
22func TestSummaryDocs(t *testing.T) {
23 t.Parallel()
24 entries := []domain.TreeEntry{
25 {Name: "readme.txt", Kind: domain.EntryFile},
26 {Name: "SECURITY.md", Kind: domain.EntryFile},
27 {Name: "LICENSE", Kind: domain.EntryExecutable},
28 {Name: "README.md", Kind: domain.EntryFile},
29 {Name: "CONTRIBUTING.md", Kind: domain.EntryDirectory},
30 {Name: "COPYING.txt", Kind: domain.EntryFile},
31 {Name: "code_of_conduct.MD", Kind: domain.EntryFile},
32 }
33 docs := summaryDocs(entries)
34 var names []string
35 for _, e := range docs {
36 names = append(names, e.Name)
37 }
38 assert.Equal(t, []string{"README.md", "LICENSE", "COPYING.txt", "code_of_conduct.MD", "SECURITY.md"}, names)
39 assert.Empty(t, summaryDocs(nil))
40}
41
42func TestSummaryDocSelection(t *testing.T) {
43 t.Parallel()
44 for _, tc := range []struct {
45 selected, want, body string
46 markdown, noReadme bool
47 }{
48 {"", "README.md", "# Read me", true, false},
49 {"security.MD", "SECURITY.md", "# Reporting", true, false},
50 {"LICENSE", "LICENSE", "<license>", false, false},
51 {"../secret", "README.md", "# Read me", true, false},
52 {"", "LICENSE", "<license>", false, true},
53 } {
54 t.Run(tc.selected+tc.want, func(t *testing.T) {
55 t.Parallel()
56 repo := repoName(t, "r")
57 browse := mocks.NewBrowseService(t)
58 entries := []domain.TreeEntry{{Name: "LICENSE", Path: "LICENSE", Kind: domain.EntryFile}, {Name: "SECURITY.md", Path: "SECURITY.md", Kind: domain.EntryFile}}
59 if !tc.noReadme {
60 entries = append(entries, domain.TreeEntry{Name: "README.md", Path: "README.md", Kind: domain.EntryFile})
61 }
62 browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: entries}, nil)
63 browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.Principal, _ domain.RepoName, _ domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) {
64 assert.Equal(t, tc.want, path.String())
65 return domain.BlobInfo{}, io.NopCloser(strings.NewReader(tc.body)), nil
66 })
67 s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: browse})
68 d := &repoData{repo: ports.RepoView{Name: repo}, head: &domain.Ref{CommitID: objectID(t, mainTip)}}
69 rd, tabs, err := s.summaryDoc(t.Context(), d, "main", tc.selected)
70 require.NoError(t, err)
71 require.NotNil(t, rd)
72 assert.Equal(t, tc.want, rd.readme.Name)
73 if tc.markdown {
74 assert.Contains(t, string(rd.readme.HTML), "<h1")
75 assert.Len(t, rd.outline, 1)
76 } else {
77 assert.Equal(t, "<pre><license></pre>", string(rd.readme.HTML))
78 assert.Empty(t, rd.outline)
79 }
80 current := 0
81 for _, tab := range tabs {
82 if tab.Current {
83 current++
84 assert.Equal(t, tc.want, tab.Name)
85 }
86 }
87 assert.Equal(t, 1, current)
88 out := renderToString(t, ReadmeCard(*rd.readme, tabs...))
89 assert.Contains(t, out, `aria-label="Repository documents"`)
90 assert.Contains(t, out, `aria-current="page"`)
91 assert.Contains(t, out, `href="?doc=LICENSE"`)
92 })
93 }
94}
95
96func TestSummaryDocLimits(t *testing.T) {
97 t.Parallel()
98 s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: mocks.NewBrowseService(t)})
99 rd, err := s.renderDoc(t.Context(), repoName(t, "r"), mainTip, "main", "", domain.TreeEntry{Name: "LICENSE", Size: maxReadmeBytes + 1})
100 require.NoError(t, err)
101 assert.Contains(t, rd.readme.Note, "too large")
102 assert.Empty(t, rd.outline)
103 out := renderToString(t, ReadmeCard(*rd.readme))
104 assert.NotContains(t, out, "doc-tabs")
105}
106
107func TestSummaryArrivalHasNoRailCursor(t *testing.T) {
108 t.Parallel()
109 repo := repoName(t, "r")
110 browse := mocks.NewBrowseService(t)
111 tip := objectID(t, mainTip)
112 browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{Refs: []domain.Ref{{Name: "refs/heads/main", ShortName: "main", Kind: domain.RefKindBranch, CommitID: tip}}}, nil)
113 browse.EXPECT().ListCommits(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.HistoryPage{Commits: []domain.Commit{{ID: tip, Summary: "recent commit"}}}, nil)
114 browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{}, nil)
115 s := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "r")), Browse: browse})
116 req := httptest.NewRequest(http.MethodGet, "/r", nil)
117 req.SetPathValue("repo", "r")
118 page, status, err := s.summary(t.Context(), req)
119 require.NoError(t, err)
120 assert.Equal(t, http.StatusOK, status)
121 out := renderToString(t, page)
122 assert.Contains(t, out, "recent commit")
123 _, rail, found := strings.Cut(out, `<aside class="rail"`)
124 require.True(t, found)
125 rail, _, _ = strings.Cut(rail, "</aside>")
126 assert.NotContains(t, rail, "data-cursor")
127 assert.NotContains(t, rail, "autofocus")
128}
129
130func TestSummaryDocExactFilename(t *testing.T) {
131 t.Parallel()
132 for _, selected := range []string{"LICENSE", "license"} {
133 t.Run(selected, func(t *testing.T) {
134 t.Parallel()
135 repo := repoName(t, "r")
136 browse := mocks.NewBrowseService(t)
137 entries := []domain.TreeEntry{{Name: "LICENSE", Path: "LICENSE", Kind: domain.EntryFile}, {Name: "license", Path: "license", Kind: domain.EntryFile}}
138 browse.EXPECT().GetTree(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything, false).Return(domain.Tree{Entries: entries}, nil)
139 browse.EXPECT().OpenBlob(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, _ domain.Principal, _ domain.RepoName, _ domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) {
140 return domain.BlobInfo{}, io.NopCloser(strings.NewReader("content of " + path.String())), nil
141 })
142 s := newTestServer(t, Config{Repos: mocks.NewRepoService(t), Browse: browse})
143 d := &repoData{repo: ports.RepoView{Name: repo}, head: &domain.Ref{CommitID: objectID(t, mainTip)}}
144 req := httptest.NewRequest(http.MethodGet, "/r?doc="+selected, nil)
145 rd, tabs, err := s.summaryDoc(t.Context(), d, "main", req.URL.Query().Get("doc"))
146 require.NoError(t, err)
147 require.NotNil(t, rd)
148 assert.Equal(t, selected, rd.readme.Name)
149 assert.Equal(t, "<pre>content of "+selected+"</pre>", string(rd.readme.HTML))
150 require.Len(t, tabs, 2)
151 for _, tab := range tabs {
152 assert.Equal(t, tab.Name == selected, tab.Current, tab.Name)
153 assert.Equal(t, "?doc="+tab.Name, tab.Href)
154 }
155 })
156 }
157}