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 <laptop>")
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}