1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "bytes"
7 "errors"
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
21func testRuns(repo domain.RepoName, n int) []domain.CIRun {
22 var runs []domain.CIRun
23 for i := n; i >= 1; i-- {
24 runs = append(runs, domain.CIRun{
25 Repo: repo, ID: int64(i), Number: int64(i), Attempt: 1, WorkflowName: "ci", Ref: "refs/heads/main",
26 Sha: strings.Repeat("a", 40), Status: domain.CICompleted, Conclusion: domain.CISuccess,
27 CreatedAt: time.Now().Add(-time.Duration(i) * time.Minute),
28 })
29 }
30 return runs
31}
32
33func servingRuns(t *testing.T, repo domain.RepoName, runs []domain.CIRun, err error) (*Server, *mocks.RunService) {
34 t.Helper()
35 m := mocks.NewRunService(t)
36 m.EXPECT().ListRuns(mock.Anything, mock.Anything, repo, ports.ListRunsParams{PageSize: recentRuns}).
37 Return(ports.RunPage{Runs: runs}, err).Maybe()
38 return newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, repo.String())), Browse: mocks.NewBrowseService(t), Runs: m}), m
39}
40
41func renderRail(t *testing.T, p SummaryPage) string {
42 t.Helper()
43 var b bytes.Buffer
44 require.NoError(t, summaryRail(p).Render(t.Context(), &b))
45 return b.String()
46}
47
48func TestSummaryRailRecentActions(t *testing.T) {
49 t.Parallel()
50 repo := repoName(t, "proj")
51 srv, _ := servingRuns(t, repo, testRuns(repo, 5), nil)
52
53 p := SummaryPage{Actions: srv.recentActions(t.Context(), repo, time.Now())}
54 require.NotNil(t, p.Actions)
55 out := renderRail(t, p)
56 assert.Contains(t, out, `id="recent-actions"`)
57 assert.Contains(t, out, "Recent actions")
58 assert.Contains(t, out, `href="/proj/actions"`, "view all")
59 assert.Equal(t, 5, strings.Count(out, `class="rr-name"`))
60 assert.Contains(t, out, `href="/proj/actions/runs/5"`)
61 assert.Contains(t, out, `aria-label="success"`, "the status icon")
62 assert.Contains(t, out, `href="/proj/tree/main"`, "the ref")
63 assert.Contains(t, out, "#5")
64 assert.NotContains(t, out, `id="recent-actions" aria-labelledby="r-actions" hidden`)
65}
66
67func TestSummaryRailRecentActionsHidden(t *testing.T) {
68 t.Parallel()
69 repo := repoName(t, "proj")
70
71 // The viewer cannot read CI: no block at all, not even to receive events.
72 srv, _ := servingRuns(t, repo, nil, domain.ErrNotFound)
73 assert.Nil(t, srv.recentActions(t.Context(), repo, time.Now()))
74 assert.NotContains(t, renderRail(t, SummaryPage{}), "recent-actions")
75
76 // A failure of the read does not break the page.
77 srv, _ = servingRuns(t, repo, nil, errors.New("boom"))
78 assert.Nil(t, srv.recentActions(t.Context(), repo, time.Now()))
79
80 // No CI on this server.
81 noCI := newTestServer(t, Config{Repos: reposServing(t, publicRepo(t, "proj")), Browse: mocks.NewBrowseService(t)})
82 assert.Nil(t, noCI.recentActions(t.Context(), repo, time.Now()))
83
84 // No run yet: the block is there for the first run, but hidden.
85 srv, _ = servingRuns(t, repo, nil, nil)
86 a := srv.recentActions(t.Context(), repo, time.Now())
87 require.NotNil(t, a)
88 assert.Contains(t, renderRail(t, SummaryPage{Actions: a}), `id="recent-actions" aria-labelledby="r-actions" hidden`)
89}
90
91func TestRunUpdatedReplacesRecentActionsOnSummary(t *testing.T) {
92 t.Parallel()
93 repo := repoName(t, "proj")
94 runs := testRuns(repo, 3)
95 runs[0].Status, runs[0].Conclusion = domain.CIInProgress, ""
96 srv, _ := servingRuns(t, repo, runs, nil)
97
98 ev := ports.WatchEvent{Event: domain.RunUpdated{Run: runs[0]}}
99 ls := &liveStream{s: srv, repo: "proj", view: "summary"}
100 got, err := ls.render(t.Context(), ev)
101 require.NoError(t, err)
102 require.Len(t, got, 1)
103 assert.Equal(t, "ci-run", got[0].event)
104 assert.True(t, strings.HasPrefix(got[0].html, `<section id="recent-actions"`), got[0].html)
105 assert.Equal(t, 3, strings.Count(got[0].html, `class="rr-name"`))
106 assert.Contains(t, got[0].html, `aria-label="running"`)
107
108 // The other pages do not get the block.
109 for _, view := range []string{"refs", "commits", "mrs"} {
110 got, err = (&liveStream{s: srv, repo: "proj", view: view}).render(t.Context(), ev)
111 require.NoError(t, err, view)
112 assert.Empty(t, got, view)
113 }
114}
115
116func TestRunUpdatedOnSummaryWhenCIIsNotReadable(t *testing.T) {
117 t.Parallel()
118 repo := repoName(t, "proj")
119 srv, _ := servingRuns(t, repo, nil, domain.ErrNotFound)
120
121 ls := &liveStream{s: srv, repo: "proj", view: "summary"}
122 got, err := ls.render(t.Context(), ports.WatchEvent{Event: domain.RunUpdated{Run: testRuns(repo, 1)[0]}})
123 require.NoError(t, err)
124 require.Len(t, got, 1)
125 assert.Contains(t, got[0].html, "hidden", "the block empties and hides, no run leaks")
126 assert.NotContains(t, got[0].html, "rr-name")
127}