v0.4.4 edcdd4c

archives_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package cachesvc
4
5import (
6	"context"
7	"io"
8	"net/http"
9	"net/url"
10	"os"
11	"path/filepath"
12	"strings"
13	"testing"
14	"time"
15
16	"bnema.dev/ermite-runner/internal/actionscache"
17)
18
19func archiveRig(t *testing.T) (*rig, *actionscache.Cache, string) {
20	t.Helper()
21	ac, err := actionscache.New(actionscache.Config{Dir: filepath.Join(t.TempDir(), "a"), Max: 1 << 20})
22	if err != nil {
23		t.Fatal(err)
24	}
25	r := newRigWith(t, Config{Max: 1 << 20, MinCharge: 1, Archives: ac})
26	key, _ := actionscache.Key("https://github.com", "o/r", strings.Repeat("a", 40))
27	if _, err := ac.Ensure(context.Background(), key, func(_ context.Context, w io.Writer) error {
28		_, err := io.WriteString(w, "tarball")
29		return err
30	}); err != nil {
31		t.Fatal(err)
32	}
33	return r, ac, key
34}
35
36// The guest reads the archive the host fetched on the channel of its job, with
37// the signed URL and nothing else; a URL for another job, another key, a stale
38// expiry or a forged signature gets nothing.
39func TestActionArchiveRoute(t *testing.T) {
40	r, ac, key := archiveRig(t)
41	a := channel(t, r.svc, "er0000000a", "job-a")
42	b := channel(t, r.svc, "er0000000b", "job-b")
43
44	loc := r.svc.ArchiveURL("job-a", key, time.Hour)
45	if !strings.HasPrefix(loc, "http://127.0.0.1:9089"+ArchivePath+key+"?") {
46		t.Fatalf("url %q", loc)
47	}
48	if code, body := archiveStatus(t, a, "GET", loc); code != http.StatusOK || body != "tarball" {
49		t.Fatalf("own channel: %d %q", code, body)
50	}
51	if code, _ := archiveStatus(t, b, "GET", loc); code != http.StatusForbidden {
52		t.Errorf("another job's channel: %d", code)
53	}
54	u, _ := url.Parse(loc)
55	otherKey, _ := actionscache.Key("https://github.com", "o/private", strings.Repeat("a", 40))
56	if code, _ := archiveStatus(t, a, "GET", strings.Replace(loc, key, otherKey, 1)); code != http.StatusForbidden {
57		t.Errorf("a signature moved to another key: %d", code)
58	}
59	q := u.Query()
60	q.Set("sig", "AAAA")
61	u.RawQuery = q.Encode()
62	if code, _ := archiveStatus(t, a, "GET", u.String()); code != http.StatusForbidden {
63		t.Errorf("forged signature: %d", code)
64	}
65	r.clk.t = r.clk.t.Add(2 * time.Hour)
66	if code, _ := archiveStatus(t, a, "GET", loc); code != http.StatusForbidden {
67		t.Errorf("expired: %d", code)
68	}
69	r.clk.t = r.clk.t.Add(-2 * time.Hour)
70
71	// A corrupt archive is never served.
72	files, _ := filepath.Glob(filepath.Join(ac.Dir(), "*.tgz"))
73	if len(files) != 1 {
74		t.Fatalf("files %v", files)
75	}
76	if err := os.WriteFile(files[0], []byte("tampered"), 0o600); err != nil {
77		t.Fatal(err)
78	}
79	if code, body := archiveStatus(t, a, "GET", loc); code != http.StatusNotFound || strings.Contains(body, "tampered") {
80		t.Errorf("corrupt: %d %q", code, body)
81	}
82}
83
84func TestActionArchiveURLNeedsACache(t *testing.T) {
85	r := newRig(t, 1<<20)
86	key, _ := actionscache.Key("https://github.com", "o/r", strings.Repeat("a", 40))
87	if u := r.svc.ArchiveURL("j", key, time.Hour); u != "" {
88		t.Errorf("url without a cache: %q", u)
89	}
90}
91
92// A channel that also serves the results service still sends the action archive
93// route to the cache: the results handler takes only the paths of neither.
94func TestActionArchiveRouteWithResults(t *testing.T) {
95	r, _, key := archiveRig(t)
96	results := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
97		http.Error(w, "results", http.StatusNotFound)
98	})
99	a := channel(t, r.svc, "er0000000a", "job-a", WithResults(results))
100	if code, body := archiveStatus(t, a, "GET", r.svc.ArchiveURL("job-a", key, time.Hour)); code != http.StatusOK || body != "tarball" {
101		t.Fatalf("archive with results: %d %q", code, body)
102	}
103}

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 4

Tags 11

↑↓ moveenter openesc close