v0.36.0 1fb2421

signature_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"bnema.dev/ermite/internal/domain"
14)
15
16const (
17	sshFP = "SHA256:abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"
18	gpgFP = "0123456789ABCDEF0123456789ABCDEF01234567"
19)
20
21func verification(f domain.SignatureFormat, s domain.SignatureStatus, fp, title string) domain.CommitVerification {
22	return domain.CommitVerification{Format: f, Status: s, Fingerprint: fp, KeyTitle: title}
23}
24
25func TestSignatureBadge(t *testing.T) {
26	t.Parallel()
27	for _, tt := range []struct {
28		name         string
29		v            domain.CommitVerification
30		long         bool
31		nilBadge     bool
32		proto, label string
33		class, title string
34	}{
35		{name: "unchecked", v: domain.CommitVerification{}, nilBadge: true},
36		{name: "unchecked long", v: domain.CommitVerification{}, long: true, nilBadge: true},
37		{name: "unsigned in a list", v: verification("", domain.StatusUnsigned, "", ""), nilBadge: true},
38		{name: "unsigned on the page", v: verification("", domain.StatusUnsigned, "", ""), long: true, label: "unsigned", class: "sig-none"},
39		{name: "verified ssh", v: verification(domain.FormatSSH, domain.StatusVerified, sshFP, "laptop"),
40			proto: "SSH", label: "Verified", class: "sig-ok", title: "Signed with SSH key " + sshFP + " (laptop) — verified for the committer email"},
41		{name: "verified gpg", v: verification(domain.FormatGPG, domain.StatusVerified, gpgFP, ""),
42			proto: "GPG", label: "Verified", class: "sig-ok", title: "Signed with GPG key " + gpgFP + " — verified for the committer email"},
43		{name: "unknown key", v: verification(domain.FormatGPG, domain.StatusUnknownKey, gpgFP, ""),
44			proto: "GPG", label: "Unverified", class: "sig-warn", title: "Signed with an unknown GPG key " + gpgFP},
45		{name: "email mismatch", v: verification(domain.FormatSSH, domain.StatusEmailMismatch, sshFP, "laptop"),
46			proto: "SSH", label: "Unverified", class: "sig-warn", title: "this key is not trusted for the committer email"},
47		{name: "expired", v: verification(domain.FormatGPG, domain.StatusExpired, gpgFP, ""),
48			proto: "GPG", label: "Unverified", class: "sig-warn", title: "expired"},
49		{name: "revoked", v: verification(domain.FormatGPG, domain.StatusRevoked, gpgFP, ""),
50			proto: "GPG", label: "Unverified", class: "sig-warn", title: "revoked"},
51		{name: "bad signature", v: verification(domain.FormatSSH, domain.StatusBadSignature, "", ""),
52			proto: "SSH", label: "Bad signature", class: "sig-bad", title: "The signature does not match the commit"},
53		{name: "x509", v: verification(domain.FormatX509, domain.StatusUnsupported, "", ""),
54			proto: "X.509", label: "Unverified", class: "sig-warn", title: "X.509 signatures are not checked"},
55	} {
56		t.Run(tt.name, func(t *testing.T) {
57			t.Parallel()
58			b := signatureBadge(tt.v, tt.long)
59			if tt.nilBadge {
60				assert.Nil(t, b)
61				return
62			}
63			require.NotNil(t, b)
64			assert.Equal(t, tt.proto, b.Proto)
65			assert.Equal(t, tt.label, b.Label)
66			assert.Equal(t, tt.class, b.Class)
67			assert.Contains(t, b.Title, tt.title)
68		})
69	}
70}
71
72func rowOf(v domain.CommitVerification) CommitRow {
73	id := objectID(&testing.T{}, "0123456789abcdef0123456789abcdef01234567")
74	return commitRow("demo", domain.Commit{
75		ID: id, Summary: "s", Verification: v,
76		Author: domain.Signature{Name: "A"}, Committer: domain.Signature{Time: time.Unix(1, 0)},
77	}, time.Unix(100, 0))
78}
79
80func TestCommitRowsShowTheSignatureBadge(t *testing.T) {
81	t.Parallel()
82	for _, tt := range []struct {
83		name string
84		v    domain.CommitVerification
85		want []string
86		none bool
87	}{
88		{name: "ssh verified", v: verification(domain.FormatSSH, domain.StatusVerified, sshFP, "laptop"),
89			want: []string{`class="pill sig sig-ok"`, `<span class="sig-proto">SSH</span> Verified`, `title="Signed with SSH key ` + sshFP}},
90		{name: "gpg verified", v: verification(domain.FormatGPG, domain.StatusVerified, gpgFP, ""),
91			want: []string{`class="pill sig sig-ok"`, `<span class="sig-proto">GPG</span> Verified`}},
92		{name: "unknown key", v: verification(domain.FormatGPG, domain.StatusUnknownKey, gpgFP, ""),
93			want: []string{`class="pill sig sig-warn"`, `GPG</span> Unverified`, `unknown GPG key`}},
94		{name: "bad signature", v: verification(domain.FormatSSH, domain.StatusBadSignature, "", ""),
95			want: []string{`class="pill sig sig-bad"`, `SSH</span> Bad signature`}},
96		{name: "unsigned", v: verification("", domain.StatusUnsigned, "", ""), none: true},
97		{name: "unchecked", v: domain.CommitVerification{}, none: true},
98	} {
99		t.Run(tt.name, func(t *testing.T) {
100			t.Parallel()
101			got := renderToString(t, CommitRows([]CommitRow{rowOf(tt.v)}))
102			if tt.none {
103				assert.NotContains(t, got, "sig")
104				assert.NotContains(t, got, "unsigned")
105				return
106			}
107			for _, w := range tt.want {
108				assert.Contains(t, got, w)
109			}
110		})
111	}
112}
113
114func TestCommitPageShowsTheSignature(t *testing.T) {
115	t.Parallel()
116	page := func(v domain.CommitVerification) string {
117		return renderToString(t, Commit(Layout{Repo: "demo"}, CommitPage{
118			SHA: "0123456789abcdef0123456789abcdef01234567", Short: "0123456", Subject: "s",
119			Sig: signatureBadge(v, true), Verification: v,
120		}))
121	}
122	got := page(verification(domain.FormatSSH, domain.StatusVerified, sshFP, "my <laptop>"))
123	assert.Contains(t, got, "<dt>signature</dt>")
124	assert.Contains(t, got, `<span class="sig-proto">SSH</span> Verified`)
125	assert.Contains(t, got, "<code>"+sshFP+"</code>")
126	assert.Contains(t, got, "my &lt;laptop&gt;")
127
128	got = page(verification(domain.FormatGPG, domain.StatusUnknownKey, gpgFP, ""))
129	assert.Contains(t, got, `sig-warn`)
130	assert.Contains(t, got, "<code>"+gpgFP+"</code>")
131	assert.Contains(t, got, `<span class="sig-why dim">The key is unknown</span>`, "the reason is text, not only a tooltip")
132	assert.Equal(t, 1, strings.Count(got, ">"+gpgFP), "the fingerprint is shown once; the pill tooltip may repeat it")
133
134	got = page(verification("", domain.StatusUnsigned, "", ""))
135	assert.Contains(t, got, "<dt>signature</dt>")
136	assert.Contains(t, got, "sig-none")
137	assert.Contains(t, got, "unsigned")
138	assert.NotContains(t, got, "<code>")
139	assert.NotContains(t, got, "sig-why")
140
141	got = page(domain.CommitVerification{})
142	assert.NotContains(t, got, "signature")
143}
144
145func TestMergeRequestCommitsShowTheSignatureBadge(t *testing.T) {
146	t.Parallel()
147	signed := signatureBadge(verification(domain.FormatGPG, domain.StatusVerified, gpgFP, ""), false)
148	got := renderToString(t, mrFooter(MRPage{Commits: []MRCommit{
149		{Short: "aaaaaaa", Href: "/r/commit/a", Subject: "signed", Sig: signed},
150		{Short: "bbbbbbb", Href: "/r/commit/b", Subject: "unsigned"},
151	}}))
152	assert.Equal(t, 1, strings.Count(got, `class="pill sig sig-ok"`), "only the signed commit has a pill")
153	assert.Contains(t, got, `<span class="sig-proto">GPG</span> Verified`)
154}

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 45

↑↓ moveenter openesc close