Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 37m ago 32290114
history blame raw 6.1 KiB
1package hook
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "rodent.software/vole/core/git"
12 "rodent.software/vole/core/lfs"
13 "rodent.software/vole/forge/repo"
14)
15
16// filesFixture builds a repository whose first commit contains every file, which
17// is enough to test what pre-receive sees at a pushed tip.
18func filesFixture(t *testing.T, files map[string][]byte) *fixture {
19 t.Helper()
20 root := t.TempDir()
21 st := repo.New(filepath.Join(root, "repos"), "/bin/true", testACLs(t, root))
22 m, err := st.Create("demo", "owner", "")
23 if err != nil {
24 t.Fatal(err)
25 }
26 a, err := m.ACL()
27 if err != nil {
28 t.Fatal(err)
29 }
30 if err := a.SetEntry("writer", "maintain"); err != nil {
31 t.Fatal(err)
32 }
33 if err := m.SetACL(a, "test"); err != nil {
34 t.Fatal(err)
35 }
36
37 g := m.Git()
38 tree, err := git.WriteFlatTree(g, files)
39 if err != nil {
40 t.Fatal(err)
41 }
42 commit, err := g.CommitTree(tree, "", "initial")
43 if err != nil {
44 t.Fatal(err)
45 }
46 return &fixture{t: t, m: m, head: commit, usersDB: testUsersDB(root)}
47}
48
49func pointerFor(content []byte) (lfs.Pointer, []byte) {
50 sum := sha256.Sum256(content)
51 p := lfs.Pointer{OID: hex.EncodeToString(sum[:]), Size: int64(len(content))}
52 return p, p.Encode()
53}
54
55func testStore(t *testing.T) *lfs.Store { return lfs.New(t.TempDir(), 1<<20) }
56
57func TestPointerWithoutObjectIsAProblem(t *testing.T) {
58 t.Parallel()
59 p, pointer := pointerFor([]byte("artifact bytes nobody uploaded"))
60 f := filesFixture(t, map[string][]byte{"artifact.bin": pointer})
61
62 code, out := f.runWith(Config{Actor: "writer", LFS: testStore(t)}, "refs/heads/main")
63 if code == 0 {
64 t.Fatalf("push accepted although the pointer names an object the server lacks:\n%s", out)
65 }
66 for _, want := range []string{p.OID[:12], "not on this server", "git lfs push"} {
67 if !strings.Contains(out, want) {
68 t.Fatalf("rejection does not mention %q:\n%s", want, out)
69 }
70 }
71}
72
73func TestPointerWithObjectIsAccepted(t *testing.T) {
74 t.Parallel()
75 content := []byte("artifact bytes that were uploaded")
76 p, pointer := pointerFor(content)
77 store := testStore(t)
78 if _, _, err := store.Put("demo", p.OID, p.Size, bytes.NewReader(content)); err != nil {
79 t.Fatal(err)
80 }
81 f := filesFixture(t, map[string][]byte{"artifact.bin": pointer})
82
83 if code, out := f.runWith(Config{Actor: "writer", LFS: store}, "refs/heads/main"); code != 0 {
84 t.Fatalf("push rejected although the object is present:\n%s", out)
85 }
86}
87
88func TestPointerSizeMismatchIsAProblem(t *testing.T) {
89 t.Parallel()
90 content := []byte("artifact bytes that were uploaded")
91 p, _ := pointerFor(content)
92 store := testStore(t)
93 if _, _, err := store.Put("demo", p.OID, p.Size, bytes.NewReader(content)); err != nil {
94 t.Fatal(err)
95 }
96 // Same object id, a size that does not describe it: a client would stall or
97 // truncate, so the pointer is not trusted.
98 lying := lfs.Pointer{OID: p.OID, Size: p.Size + 10}.Encode()
99 f := filesFixture(t, map[string][]byte{"artifact.bin": lying})
100
101 code, out := f.runWith(Config{Actor: "writer", LFS: store}, "refs/heads/main")
102 if code == 0 {
103 t.Fatalf("push accepted with a pointer that misstates the size:\n%s", out)
104 }
105 if !strings.Contains(out, "bytes") {
106 t.Fatalf("rejection does not explain the size mismatch:\n%s", out)
107 }
108}
109
110func TestLargeBinaryIsWarnedAboutNotRejected(t *testing.T) {
111 t.Parallel()
112 binary := append(bytes.Repeat([]byte{0x00, 0x01, 0x02, 0x03}, 2000), 0xff)
113 f := filesFixture(t, map[string][]byte{
114 "assets/logo.png": binary,
115 "README.md": []byte("text\n"),
116 })
117
118 code, out := f.runWith(Config{Actor: "writer", LFS: testStore(t), LFSWarnBytes: 1024}, "refs/heads/main")
119 if code != 0 {
120 t.Fatalf("a large binary should be allowed with a warning, not rejected:\n%s", out)
121 }
122 for _, want := range []string{"warning", "assets/logo.png", `git lfs track "*.png"`, "git lfs migrate"} {
123 if !strings.Contains(out, want) {
124 t.Fatalf("warning does not mention %q:\n%s", want, out)
125 }
126 }
127}
128
129func TestTextFileIsNotWarnedAbout(t *testing.T) {
130 t.Parallel()
131 text := []byte(strings.Repeat("a line of prose\n", 500))
132 f := filesFixture(t, map[string][]byte{"notes.md": text})
133 code, out := f.runWith(Config{Actor: "writer", LFS: testStore(t), LFSWarnBytes: 1024}, "refs/heads/main")
134 if code != 0 {
135 t.Fatalf("push rejected: %s", out)
136 }
137 if strings.Contains(out, "warning") {
138 t.Fatalf("a text file was flagged as a binary to move to LFS:\n%s", out)
139 }
140}
141
142func TestDeletedPointerDoesNotBlockItsOwnRemoval(t *testing.T) {
143 t.Parallel()
144 // A broken pointer already in the tree must be removable; otherwise the only
145 // way out of the mistake is rewriting history.
146 root := t.TempDir()
147 st := repo.New(filepath.Join(root, "repos"), "/bin/true", testACLs(t, root))
148 m, err := st.Create("demo", "owner", "")
149 if err != nil {
150 t.Fatal(err)
151 }
152 a, err := m.ACL()
153 if err != nil {
154 t.Fatal(err)
155 }
156 if err := a.SetEntry("writer", "maintain"); err != nil {
157 t.Fatal(err)
158 }
159 if err := m.SetACL(a, "test"); err != nil {
160 t.Fatal(err)
161 }
162 g := m.Git()
163 _, pointer := pointerFor([]byte("never uploaded"))
164 badBlob, err := g.HashObject(pointer)
165 if err != nil {
166 t.Fatal(err)
167 }
168 goodBlob, err := g.HashObject([]byte("text\n"))
169 if err != nil {
170 t.Fatal(err)
171 }
172 clutter, err := g.MkTree([]git.Entry{
173 {Mode: "100644", Type: "blob", SHA: badBlob, Name: "artifact.bin"},
174 {Mode: "100644", Type: "blob", SHA: goodBlob, Name: "README.md"},
175 })
176 if err != nil {
177 t.Fatal(err)
178 }
179 c1, err := g.CommitTree(clutter, "", "add a pointer with no object")
180 if err != nil {
181 t.Fatal(err)
182 }
183 clean, err := g.MkTree([]git.Entry{
184 {Mode: "100644", Type: "blob", SHA: goodBlob, Name: "README.md"},
185 })
186 if err != nil {
187 t.Fatal(err)
188 }
189 c2, err := g.CommitTree(clean, c1, "remove it")
190 if err != nil {
191 t.Fatal(err)
192 }
193
194 var out bytes.Buffer
195 code := PreReceive(Config{
196 RepoDir: m.Dir,
197 Actor: "writer",
198 LFS: testStore(t),
199 // The rules live in the instance database, so the gate needs it even when a
200 // test is only about pointers.
201 UsersDB: testUsersDB(root),
202 In: strings.NewReader(git.ZeroSHA + " " + c2 + " refs/heads/main\n"),
203 Out: &out,
204 })
205 if code != 0 {
206 t.Fatalf("removing a broken pointer was rejected:\n%s", out.String())
207 }
208}
209