v0.14.0 815fb98

actions_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"context"
7	"net/http"
8	"net/http/httptest"
9	"net/netip"
10	"testing"
11	"time"
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/clientlimit"
18	"bnema.dev/ermite/internal/domain"
19	"bnema.dev/ermite/internal/ports"
20	"bnema.dev/ermite/internal/ports/mocks"
21)
22
23// S6: every page that streams a log through the use case names the visitor, so
24// that the limit of log streams per client applies to the pages too, behind a
25// trusted proxy by the address it forwards.
26func TestLogPagesPassTheClientKey(t *testing.T) {
27	t.Parallel()
28	const jobID = "1-0-0"
29	repo := repoName(t, "proj")
30	job := ports.JobView{CIJob: domain.CIJob{
31		ID: jobID, RunID: 1, Attempt: 1, RunAttempt: 1, Repo: repo, Key: "build", Name: "build",
32		Phase: domain.CIPhaseEnded, Conclusion: domain.CISuccess,
33	}, Lines: 1}
34	want := clientlimit.Key("198.51.100.7")
35
36	runs := mocks.NewRunService(t)
37	runs.EXPECT().GetJob(mock.Anything, mock.Anything, repo, jobID, int32(0)).Return(job, nil).Maybe()
38	runs.EXPECT().GetRun(mock.Anything, mock.Anything, repo, int64(1), int32(0)).Return(ports.RunDetail{
39		Run: domain.CIRun{Repo: repo, ID: 1, Attempt: 1}, LatestAttempt: 1,
40	}, nil).Maybe()
41	runs.EXPECT().ListAnnotations(mock.Anything, mock.Anything, repo, int64(1), int32(0), jobID).Return(ports.AnnotationList{}, nil).Maybe()
42	var seen []string
43	runs.EXPECT().GetJobLog(mock.Anything, mock.Anything, repo, mock.Anything, mock.Anything).RunAndReturn(
44		func(_ context.Context, _ domain.Principal, _ domain.RepoName, p ports.GetJobLogParams, send func(ports.LogBatch) error) error {
45			seen = append(seen, p.Client)
46			return send(ports.LogBatch{Lines: []ports.JobLogLine{{Step: 1, CILogLine: domain.CILogLine{Time: time.Unix(0, 0), Text: "x"}}}})
47		})
48
49	browse := mocks.NewBrowseService(t)
50	browse.EXPECT().ListRefs(mock.Anything, mock.Anything, repo, mock.Anything).Return(ports.RefPage{}, nil).Maybe()
51	srv := newTestServer(t, Config{
52		Repos: reposServing(t, publicRepo(t, "proj")), Browse: browse, Runs: runs,
53		TrustedProxies: []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")},
54	})
55	for _, path := range []string{
56		"/proj/actions/runs/1/job/" + jobID,
57		"/proj/actions/runs/1/job/" + jobID + "/raw",
58		"/proj/actions/runs/1/job/" + jobID + "/stream",
59	} {
60		req := httptest.NewRequest(http.MethodGet, path, nil)
61		req.RemoteAddr = "192.0.2.1:4000"
62		req.Header.Set("X-Forwarded-For", "198.51.100.7")
63		rec := httptest.NewRecorder()
64		srv.ServeHTTP(rec, req)
65		require.Equal(t, http.StatusOK, rec.Code, path)
66	}
67	require.Len(t, seen, 3)
68	for _, c := range seen {
69		assert.Equal(t, want, c)
70	}
71}
72
73// M4: one helper shapes the lines of the page, the raw feed and the live feed.
74func TestAppendLogLine(t *testing.T) {
75	t.Parallel()
76	at := time.Date(2026, 10, 12, 10, 11, 12, 0, time.UTC)
77	line := func(text string, cont bool) ports.JobLogLine {
78		return ports.JobLogLine{Step: 2, CILogLine: domain.CILogLine{Time: at, Text: text, Continued: cont}}
79	}
80	var out []LogLineView
81	out = appendLogLine(out, 10, line("##[group]Run make", false))
82	out = appendLogLine(out, 11, line("plain <b>", false))
83	out = appendLogLine(out, 12, line("##[error]Process completed with exit code 1.", false))
84	out = appendLogLine(out, 13, line("##[endgroup]", false))
85	require.Len(t, out, 4)
86	assert.True(t, out[0].Group)
87	assert.Equal(t, "Run make", out[0].Text, "the marker is structure, not text")
88	assert.EqualValues(t, 10, out[0].N)
89	assert.Equal(t, "10:11:12", out[0].Time)
90	assert.EqualValues(t, 2, out[0].Step)
91	assert.False(t, out[1].Group || out[1].Error || out[1].End)
92	assert.True(t, out[2].Error)
93	assert.Equal(t, "Process completed with exit code 1.", out[2].Text)
94	assert.True(t, out[3].End)
95
96	// A cut line comes back as one line, with the number of its first piece.
97	var cut []LogLineView
98	cut = appendLogLine(cut, 20, line("first half, ", false))
99	cut = appendLogLine(cut, 21, line("second half", true))
100	require.Len(t, cut, 1)
101	assert.Equal(t, "first half, second half", cut[0].Text)
102	assert.EqualValues(t, 20, cut[0].N)
103
104	// A piece that opens a batch has no line before it here: it stays, marked,
105	// for the page to join to the line it already holds.
106	open := appendLogLine(nil, 30, line("rest", true))
107	require.Len(t, open, 1)
108	assert.True(t, open[0].Continued)
109
110	// Invisible characters of the runner's text are shown, never applied.
111	hidden := appendLogLine(nil, 40, line("a\u202eb", false))
112	assert.NotContains(t, hidden[0].Text, "\u202e")
113}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close