v0.18.0 0cea275

raw_test.go RawHistoryFolder

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
16	"bnema.dev/ermite/internal/domain"
17	"bnema.dev/ermite/internal/ports"
18	"bnema.dev/ermite/internal/ports/mocks"
19)
20
21const (
22	blobID   = "0123456789abcdef0123456789abcdef01234567"
23	mainTip  = "c0ffee0c0ffee0c0ffee0c0ffee0c0ffee0c0ffe"
24	blobBody = "hello"
25)
26
27// rawBrowse serves one text file, the only call raw makes besides the refs.
28func rawBrowse(t *testing.T) *mocks.BrowseService {
29	t.Helper()
30	browse := mocks.NewBrowseService(t)
31	browse.EXPECT().ListRefs(mock.Anything, domain.Anonymous(), repoName(t, "r"), mock.Anything).Return(ports.RefPage{
32		Refs: []domain.Ref{{
33			Name: "refs/heads/main", ShortName: "main", Kind: domain.RefKindBranch,
34			ID: objectID(t, mainTip), CommitID: objectID(t, mainTip),
35		}},
36	}, nil).Maybe()
37	browse.EXPECT().OpenBlob(mock.Anything, domain.Anonymous(), repoName(t, "r"), mock.Anything, mock.Anything).RunAndReturn(
38		func(_ context.Context, _ domain.Principal, _ domain.RepoName, rev domain.Revision, path domain.TreePath) (domain.BlobInfo, io.ReadCloser, error) {
39			if path.String() != "docs/a.txt" || rev.String() != mainTip {
40				return domain.BlobInfo{}, nil, domain.ErrNotFound
41			}
42			return domain.BlobInfo{ID: objectID(t, blobID), Path: "docs/a.txt", Size: int64(len(blobBody)), MediaType: "text/plain"},
43				io.NopCloser(strings.NewReader(blobBody)), nil
44		}).Maybe()
45	return browse
46}
47
48// A revalidation of an unchanged file ends with a 304 and no body. The blob is
49// opened in every case: its id is only known from the metadata the use case
50// returns with the content.
51func TestRawRevalidation(t *testing.T) {
52	t.Parallel()
53	tests := []struct {
54		name   string
55		url    string
56		method string
57		ifNone string
58		want   int
59		body   string
60	}{
61		{"no validator", "/r/raw/main/docs/a.txt", http.MethodGet, "", http.StatusOK, "hello"},
62		{"matching validator", "/r/raw/main/docs/a.txt", http.MethodGet, `"` + blobID + `"`, http.StatusNotModified, ""},
63		{"other validator", "/r/raw/main/docs/a.txt", http.MethodGet, `"ffffffffffffffffffffffffffffffffffffffff"`, http.StatusOK, "hello"},
64		{"weak validator matches (weak comparison)", "/r/raw/main/docs/a.txt", http.MethodGet, `W/"` + blobID + `"`, http.StatusNotModified, ""},
65		{"list of validators", "/r/raw/main/docs/a.txt", http.MethodGet, `"other", "` + blobID + `"`, http.StatusNotModified, ""},
66		{"any validator", "/r/raw/main/docs/a.txt", http.MethodGet, "*", http.StatusNotModified, ""},
67		{"list without a match", "/r/raw/main/docs/a.txt", http.MethodGet, `"other", "another"`, http.StatusOK, "hello"},
68		{"head", "/r/raw/main/docs/a.txt", http.MethodHead, "", http.StatusOK, ""},
69		{"missing file", "/r/raw/main/docs/none.txt", http.MethodGet, `"` + blobID + `"`, http.StatusNotFound, ""},
70		{"directory", "/r/raw/main", http.MethodGet, `"` + blobID + `"`, http.StatusNotFound, ""},
71	}
72	for _, tt := range tests {
73		t.Run(tt.name, func(t *testing.T) {
74			t.Parallel()
75			repos := reposServing(t, publicRepo(t, "r"))
76			srv := newTestServer(t, Config{Repos: repos, Browse: rawBrowse(t)})
77			req := httptest.NewRequest(tt.method, tt.url, nil)
78			if tt.ifNone != "" {
79				req.Header.Set("If-None-Match", tt.ifNone)
80			}
81			rec := httptest.NewRecorder()
82			srv.ServeHTTP(rec, req)
83
84			assert.Equal(t, tt.want, rec.Code)
85			if tt.want != http.StatusNotFound {
86				assert.Equal(t, tt.body, rec.Body.String())
87			}
88			if tt.want == http.StatusNotModified {
89				assert.Equal(t, `"`+blobID+`"`, rec.Header().Get("ETag"))
90				assert.Equal(t, "no-cache", rec.Header().Get("Cache-Control"))
91			}
92			if tt.want == http.StatusOK {
93				assert.Equal(t, `"`+blobID+`"`, rec.Header().Get("ETag"))
94				assert.Equal(t, rawCSP, rec.Header().Get("Content-Security-Policy"))
95			}
96		})
97	}
98}

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