main 80d5067

web_release_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"context"
7	"encoding/json"
8	"io"
9	"net/http"
10	"strconv"
11	"strings"
12	"testing"
13
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/require"
16
17	"bnema.dev/ermite/internal/domain"
18	"bnema.dev/ermite/internal/ports"
19	"bnema.dev/ermite/internal/usecase"
20)
21
22// Releases and web use the production forge wiring.
23func TestWebRelease(t *testing.T) {
24	h := newHarness(t, harnessOpts{})
25	h.webRepo("demo")
26	blobs, releases := h.forge.Blobs, h.forge.Releases
27	repo := h.name("demo")
28	rel, err := releases.CreateRelease(h.ctx, domain.Root(), repo, ports.CreateReleaseParams{Tag: "v0.2.0", Title: "Demo release", Notes: "## Install\n\n**Download** the binary. <script>alert(1)</script>"})
29	require.NoError(t, err)
30	payload := "binary release data\x00\xff\n"
31	asset, err := releases.UploadAsset(h.ctx, domain.Root(), repo, ports.UploadAssetParams{ReleaseID: rel.ID, Name: "demo.bin", ContentType: "application/octet-stream", Size: int64(len(payload))}, strings.NewReader(payload))
32	require.NoError(t, err)
33	draft, err := releases.CreateRelease(h.ctx, domain.Root(), repo, ports.CreateReleaseParams{Tag: "draft", Title: "Hidden draft", Draft: true})
34	require.NoError(t, err)
35	draftAsset, err := releases.UploadAsset(h.ctx, domain.Root(), repo, ports.UploadAssetParams{ReleaseID: draft.ID, Name: "hidden.bin", Size: 1}, strings.NewReader("x"))
36	require.NoError(t, err)
37	ts := h.srv
38	get := func(method, path, byteRange string) (int, http.Header, string) {
39		t.Helper()
40		req, err := http.NewRequestWithContext(t.Context(), method, ts.URL+path, nil)
41		require.NoError(t, err)
42		if byteRange != "" {
43			req.Header.Set("Range", byteRange)
44		}
45		res, err := ts.Client().Do(req)
46		require.NoError(t, err)
47		defer func() { _ = res.Body.Close() }()
48		body, err := io.ReadAll(res.Body)
49		require.NoError(t, err)
50		return res.StatusCode, res.Header, string(body)
51	}
52	status, _, body := get("GET", "/demo/releases", "")
53	require.Equal(t, 200, status)
54	assert.Contains(t, body, "Demo release")
55	assert.NotContains(t, body, "Hidden draft")
56	status, _, body = get("GET", "/demo/releases/tag/v0.2.0", "")
57	require.Equal(t, 200, status)
58	assert.Contains(t, body, "<strong>Download</strong>")
59	assert.NotContains(t, body, "<script>alert")
60	assert.Contains(t, body, "demo.bin")
61	assert.Contains(t, body, "/demo/releases/assets/"+strconv.FormatInt(asset.ID, 10))
62	for _, path := range []string{"/demo/releases/download/v0.2.0/demo.bin", "/demo/releases/latest/download/demo.bin", "/demo/releases/assets/" + strconv.FormatInt(asset.ID, 10)} {
63		status, headers, body := get("GET", path, "")
64		require.Equal(t, 200, status)
65		assert.Equal(t, payload, body)
66		assert.Equal(t, strconv.Itoa(len(payload)), headers.Get("Content-Length"))
67		assert.Equal(t, "bytes", headers.Get("Accept-Ranges"))
68		assert.Contains(t, headers.Get("Content-Disposition"), "attachment")
69		status, headers, body = get("GET", path, "bytes=2-6")
70		require.Equal(t, 206, status)
71		assert.Equal(t, payload[2:7], body)
72		assert.Equal(t, "bytes 2-6/"+strconv.Itoa(len(payload)), headers.Get("Content-Range"))
73		assert.Equal(t, "5", headers.Get("Content-Length"))
74		status, headers, body = get("HEAD", path, "")
75		require.Equal(t, 200, status)
76		assert.Empty(t, body)
77		assert.Equal(t, strconv.Itoa(len(payload)), headers.Get("Content-Length"))
78	}
79	for _, path := range []string{"/demo/releases/tag/draft", "/demo/releases/download/draft/hidden.bin", "/demo/releases/assets/" + strconv.FormatInt(draftAsset.ID, 10)} {
80		status, _, _ := get("GET", path, "")
81		assert.Equal(t, 404, status)
82	}
83	// Slash-tag page and asset-id download remain unambiguous.
84	slash, err := releases.CreateRelease(h.ctx, domain.Root(), repo, ports.CreateReleaseParams{Tag: "series/v1", TargetCommitish: "main", Title: "Slash tag"})
85	require.NoError(t, err)
86	status, _, body = get("GET", "/demo/releases/tag/series/v1", "")
87	assert.Equal(t, 200, status)
88	assert.Contains(t, body, slash.Title)
89	// The browser_download_url of the REST API for a slash tag is served by
90	// the web route.
91	slashAsset, err := releases.UploadAsset(h.ctx, domain.Root(), repo, ports.UploadAssetParams{ReleaseID: slash.ID, Name: "s.bin", Size: 5}, strings.NewReader("slash"))
92	require.NoError(t, err)
93	gh := usecase.NewReleasesGitHub(releases, "owner", ts.URL)
94	raw, err := gh.GetAsset(h.ctx, domain.Root(), "owner", "demo", slashAsset.ID)
95	require.NoError(t, err)
96	var obj struct {
97		URL string `json:"browser_download_url"`
98	}
99	require.NoError(t, json.Unmarshal(raw, &obj))
100	require.Equal(t, ts.URL+"/demo/releases/download/series%2Fv1/s.bin", obj.URL)
101	status, _, body = get("GET", strings.TrimPrefix(obj.URL, ts.URL), "")
102	assert.Equal(t, 200, status)
103	assert.Equal(t, "slash", body)
104	// A missing blob is a 404 page before any binary headers are written.
105	require.NoError(t, blobs.Delete(context.Background(), asset.BlobKey()))
106	status, headers, body := get("GET", "/demo/releases/assets/"+strconv.FormatInt(asset.ID, 10), "")
107	assert.Equal(t, 404, status)
108	assert.Contains(t, headers.Get("Content-Type"), "text/html")
109	assert.Contains(t, body, "no such page")
110	assert.Empty(t, headers.Get("Content-Disposition"))
111}

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 3

Tags 44

↑↓ moveenter openesc close