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.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 26m ago
4b8c9e1d
| 1 | package client |
| 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 |
| 5 | // lfs, and a mock would agree with whatever this file happened to write. |
| 6 | |
| 7 | import ( |
| 8 | "crypto/sha256" |
| 9 | "encoding/hex" |
| 10 | "io" |
| 11 | "net" |
| 12 | "os" |
| 13 | "path/filepath" |
| 14 | "testing" |
| 15 | |
| 16 | "rodent.software/vole/core/lfs" |
| 17 | ) |
| 18 | |
| 19 | // transferSession starts the server side of an upload session over a pipe and |
| 20 | // returns a client holding the other end. |
| 21 | func transferSession(t *testing.T, store *lfs.Store) (*Transfer, func()) { |
| 22 | t.Helper() |
| 23 | client, server := net.Pipe() |
| 24 | tx := &Transfer{p: newPktConn(client, client)} |
| 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. |
| 69 | f, size, err := store.Open("demo", ptr.OID) |
| 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 { |
| 128 | host, port, path, err := ParseSSHRemote(c.remote) |
| 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 | } |
| 145 |