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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Blame · client/transfer_test.go

Line attribution on main · 144 line(s)

c5902451
Keenan Nemetz
16d ago
1 package client
85cc89fe
Keenan Nemetz
17d ago
2
3 // The protocol client is tested against the real server handler rather than a
4 // mock: the framing, the status shape and the batch answer all have to agree with
c5902451
Keenan Nemetz
16d ago
5 // lfs, and a mock would agree with whatever this file happened to write.
85cc89fe
Keenan Nemetz
17d ago
6
7 import (
8 "crypto/sha256"
9 "encoding/hex"
10 "io"
11 "net"
12 "os"
13 "path/filepath"
14 "testing"
15
d37d301f
Keenan Nemetz
15h ago
16 "rodent.software/vole/core/lfs"
85cc89fe
Keenan Nemetz
17d ago
17 )
18
19 // transferSession starts the server side of an upload session over a pipe and
20 // returns a client holding the other end.
c5902451
Keenan Nemetz
16d ago
21 func transferSession(t *testing.T, store *lfs.Store) (*Transfer, func()) {
85cc89fe
Keenan Nemetz
17d ago
22 t.Helper()
23 client, server := net.Pipe()
c5902451
Keenan Nemetz
16d ago
24 tx := &Transfer{p: newPktConn(client, client)}
85cc89fe
Keenan Nemetz
17d ago
25 done := make(chan struct{})
26 go func() {
27 defer close(done)
28 tr := &lfs.Transfer{Store: store, Repo: "demo", CanRead: true, CanWrite: true}
29 _ = tr.Serve("upload", server, server)
30 }()
31 if err := tx.hello(); err != nil {
32 t.Fatalf("version negotiation: %v", err)
33 }
34 return tx, func() {
35 _ = client.Close()
36 <-done
37 }
38 }
39
40 func writeTempFile(t *testing.T, name string, data []byte) string {
41 t.Helper()
42 p := filepath.Join(t.TempDir(), name)
43 if err := os.WriteFile(p, data, 0o644); err != nil {
44 t.Fatal(err)
45 }
46 return p
47 }
48
49 func TestTransferClientUploadsAndVerifies(t *testing.T) {
50 store := lfs.New(t.TempDir(), 1<<20)
51 data := []byte("a signed release binary, conceptually\n")
52 path := writeTempFile(t, "vole-linux-amd64", data)
53
54 tx, done := transferSession(t, store)
55 ptr, err := tx.Upload(path)
56 done()
57 if err != nil {
58 t.Fatalf("upload: %v", err)
59 }
60
61 sum := sha256.Sum256(data)
62 if want := hex.EncodeToString(sum[:]); ptr.OID != want {
63 t.Fatalf("oid = %s, want %s", ptr.OID, want)
64 }
65 if ptr.Size != int64(len(data)) {
66 t.Fatalf("size = %d, want %d", ptr.Size, len(data))
67 }
68 // The pointer's claim is checked against the store, not against the client.
65b88b3b
Keenan Nemetz
16d ago
69 f, size, err := store.Open("demo", ptr.OID)
85cc89fe
Keenan Nemetz
17d ago
70 if err != nil {
71 t.Fatalf("object is not in the store: %v", err)
72 }
73 defer f.Close()
74 got, err := io.ReadAll(f)
75 if err != nil {
76 t.Fatal(err)
77 }
78 if string(got) != string(data) || size != int64(len(data)) {
79 t.Fatalf("stored %d bytes (%d claimed), want the file's %d", len(got), size, len(data))
80 }
81 }
82
83 // TestTransferClientSkipsAnObjectTheStoreHas is the content-addressing payoff: the
84 // second attach of the same bytes transfers nothing, which is what makes
85 // re-uploading a 100 MiB binary after a failed push cheap.
86 func TestTransferClientSkipsAnObjectTheStoreHas(t *testing.T) {
87 store := lfs.New(t.TempDir(), 1<<20)
88 data := []byte("identical bytes, second upload\n")
89 path := writeTempFile(t, "artifact", data)
90
91 tx, done := transferSession(t, store)
92 first, err := tx.Upload(path)
93 done()
94 if err != nil {
95 t.Fatalf("first upload: %v", err)
96 }
97
98 tx, done = transferSession(t, store)
99 second, err := tx.Upload(path)
100 done()
101 if err != nil {
102 t.Fatalf("second upload: %v", err)
103 }
104 if first.OID != second.OID || first.Size != second.Size {
105 t.Fatalf("second attach produced %+v, want %+v", second, first)
106 }
107 objects, bytes, err := store.Usage()
108 if err != nil {
109 t.Fatal(err)
110 }
111 if objects != 1 || bytes != int64(len(data)) {
112 t.Fatalf("store holds %d objects / %d bytes, want 1 / %d", objects, bytes, len(data))
113 }
114 }
115
116 func TestParseSSHRemote(t *testing.T) {
117 cases := []struct {
118 remote, host, port, path string
119 wantErr bool
120 }{
121 {remote: "ssh://git@example.com/demo.git", host: "git@example.com", path: "/demo.git"},
122 {remote: "ssh://git@example.com:2299/demo.git", host: "git@example.com", port: "2299", path: "/demo.git"},
123 {remote: "git@example.com:demo.git", host: "git@example.com", path: "/demo.git"},
124 {remote: "ssh://example.com", wantErr: true},
125 {remote: "https://example.com/demo.git", wantErr: true},
126 }
127 for _, c := range cases {
c5902451
Keenan Nemetz
16d ago
128 host, port, path, err := ParseSSHRemote(c.remote)
85cc89fe
Keenan Nemetz
17d ago
129 if c.wantErr {
130 if err == nil {
131 t.Errorf("%q parsed as %s %s %s, want an error", c.remote, host, port, path)
132 }
133 continue
134 }
135 if err != nil {
136 t.Errorf("%q: %v", c.remote, err)
137 continue
138 }
139 if host != c.host || port != c.port || path != c.path {
140 t.Errorf("%q -> host=%q port=%q path=%q, want %q %q %q",
141 c.remote, host, port, path, c.host, c.port, c.path)
142 }
143 }
144 }