tinyrelayfile
tinyrelay/internal/blob/path_upload_test.go/

internal/blob/path_upload_test.go raw
1package blob
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "io"
9 "net"
10 "net/http"
11 "net/http/httptest"
12 "strings"
13 "testing"
14)
15
16func TestPathUploadStoresExactBytesAndReturnsCreatedThenOK(t *testing.T) {
17 s := testService(t)
18 body := []byte("BUD-13 bytes")
19 digest := sha256.Sum256(body)
20 sha := hex.EncodeToString(digest[:])
21 request := httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha+".txt", strings.NewReader(string(body)))
22 request.Header.Set("Content-Type", "text/plain")
23 first := record(s.Handler(), request)
24 if first.Code != http.StatusCreated || !strings.Contains(first.Body.String(), sha) {
25 t.Fatalf("first path upload = %d %s", first.Code, first.Body.String())
26 }
27 second := record(s.Handler(), httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha, strings.NewReader(string(body))))
28 if second.Code != http.StatusOK {
29 t.Fatalf("duplicate path upload = %d %s", second.Code, second.Body.String())
30 }
31}
32
33func TestPathUploadRejectsHashMismatchWithoutPersisting(t *testing.T) {
34 s := testService(t)
35 wanted := strings.Repeat("a", 64)
36 response := record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, wanted) }, httptest.NewRequest(http.MethodPut, "https://relay.test/"+wanted, strings.NewReader("wrong")))
37 if response.Code != http.StatusConflict {
38 t.Fatalf("mismatch status = %d, want 409", response.Code)
39 }
40 if _, _, err := s.Get(context.Background(), wanted); err == nil {
41 t.Fatal("mismatched path upload was persisted")
42 }
43}
44
45func TestPathUploadAuthenticatesBeforeReadingBody(t *testing.T) {
46 s := testService(t)
47 read := false
48 s.config.Authorize = func(*http.Request, Action) (string, error) { return "", errors.New("bad auth") }
49 body := &trackingReader{Reader: strings.NewReader("should not be read"), read: &read}
50 sha := strings.Repeat("b", 64)
51 response := record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, sha) }, httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha, body))
52 if response.Code != http.StatusUnauthorized || read {
53 t.Fatalf("auth status/read = %d/%v", response.Code, read)
54 }
55}
56
57func TestPathUploadRejectsRemoteBodyAndMalformedURL(t *testing.T) {
58 s := testService(t)
59 sha := strings.Repeat("c", 64)
60 request := httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha+"?url=https%3A%2F%2Fexample.com%2Fblob", strings.NewReader("body"))
61 response := record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, sha) }, request)
62 if response.Code != http.StatusBadRequest {
63 t.Fatalf("remote body status = %d, want 400", response.Code)
64 }
65 request = httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha+"?url=file%3A%2F%2F%2Fetc%2Fpasswd", http.NoBody)
66 response = record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, sha) }, request)
67 if response.Code != http.StatusBadRequest {
68 t.Fatalf("malformed URL status = %d, want 400", response.Code)
69 }
70 request = httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha+"?url=https%3A%2F%2Fexample.com%2Fx%23fragment", http.NoBody)
71 response = record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, sha) }, request)
72 if response.Code != http.StatusBadRequest {
73 t.Fatalf("fragment URL status = %d, want 400", response.Code)
74 }
75}
76
77func TestPathUploadRemoteSourceVerifiesHashAndSupportsHTTP(t *testing.T) {
78 s := testService(t)
79 body := []byte("remote BUD-13 bytes")
80 digest := sha256.Sum256(body)
81 sha := hex.EncodeToString(digest[:])
82 s.config.ResolveIP = func(context.Context, string) ([]net.IP, error) { return []net.IP{net.ParseIP("8.8.8.8")}, nil }
83 s.resolve = s.config.ResolveIP
84 s.config.HTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
85 return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(string(body))), Header: http.Header{"Content-Type": []string{"text/plain"}}, Request: req}, nil
86 })}
87 request := httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha+"?url=http%3A%2F%2Forigin.example%2Ffile.txt", http.NoBody)
88 response := record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, sha) }, request)
89 if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), sha) {
90 t.Fatalf("remote upload = %d %s", response.Code, response.Body.String())
91 }
92}
93
94func TestPathUploadRemoteRejectsPrivateOrigin(t *testing.T) {
95 s := testService(t)
96 s.config.ResolveIP = func(context.Context, string) ([]net.IP, error) { return []net.IP{net.ParseIP("127.0.0.1")}, nil }
97 s.resolve = s.config.ResolveIP
98 response := record(func(w http.ResponseWriter, r *http.Request) { s.pathUpload(w, r, strings.Repeat("d", 64)) }, httptest.NewRequest(http.MethodPut, "https://relay.test/"+strings.Repeat("d", 64)+"?url=http%3A%2F%2Forigin.example%2Fx", http.NoBody))
99 if response.Code != http.StatusBadRequest {
100 t.Fatalf("private origin status = %d, want 400", response.Code)
101 }
102}
103
104func TestPathUploadRemoteUsesRequestContext(t *testing.T) {
105 s := testService(t)
106 sha := strings.Repeat("e", 64)
107 s.config.ResolveIP = func(ctx context.Context, _ string) ([]net.IP, error) {
108 return []net.IP{net.ParseIP("8.8.8.8")}, nil
109 }
110 s.resolve = s.config.ResolveIP
111 s.config.HTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
112 return nil, req.Context().Err()
113 })}
114 ctx, cancel := context.WithCancel(context.Background())
115 cancel()
116 request := httptest.NewRequest(http.MethodPut, "https://relay.test/"+sha+"?url=https%3A%2F%2Forigin.example%2Ffile", http.NoBody).WithContext(ctx)
117 response := record(s.Handler(), request)
118 if response.Code != http.StatusBadGateway {
119 t.Fatalf("cancelled remote upload = %d, want 502", response.Code)
120 }
121}
122
123type trackingReader struct {
124 io.Reader
125 read *bool
126}
127
128func (r *trackingReader) Read(p []byte) (int, error) {
129 *r.read = true
130 return r.Reader.Read(p)
131}
132
133func TestPathUploadPreservesAccessPurposeAndClaimMetadata(t *testing.T) {
134 s := testService(t)
135 body := "named member-only file"
136 digest := sha256.Sum256([]byte(body))
137 hash := hex.EncodeToString(digest[:])
138 r := httptest.NewRequest(http.MethodPut, "https://relay.test/"+hash+"?filename=note.txt&path=docs%2Fnote.txt&access=members&purpose=file", strings.NewReader(body))
139 r.Header.Set("Content-Type", "text/plain")
140 response := record(s.Handler(), r)
141 if response.Code != http.StatusCreated {
142 t.Fatalf("upload: %d %s", response.Code, response.Body.String())
143 }
144 entries, err := s.ListClaimMetadata(t.Context(), "uploader")
145 if err != nil || len(entries) != 1 || entries[0].Purpose != "file" || entries[0].Name != "note.txt" || entries[0].Path != "docs/note.txt" {
146 t.Fatalf("claim: %+v %v", entries, err)
147 }
148 if access, err := s.BlobAccess(t.Context(), hash); err != nil || access != AccessMembers {
149 t.Fatalf("access: %q %v", access, err)
150 }
151}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs