127 lines
4.5 KiB
Go
127 lines
4.5 KiB
Go
package submit
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"math/rand"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// sumOf is the SHA-256 a client sends with body.
|
|
func sumOf(body string) []byte {
|
|
s := sha256.Sum256([]byte(body))
|
|
return s[:]
|
|
}
|
|
|
|
func TestVerifyDigest(t *testing.T) {
|
|
t.Run("bytes that hash to the digest end cleanly", func(t *testing.T) {
|
|
b, err := io.ReadAll(VerifyDigest(strings.NewReader("modpack"), sumOf("modpack")))
|
|
if err != nil || string(b) != "modpack" {
|
|
t.Fatalf("ReadAll = (%q, %v), want (modpack, nil)", b, err)
|
|
}
|
|
})
|
|
t.Run("bytes that do not end in ErrDigestMismatch", func(t *testing.T) {
|
|
b, err := io.ReadAll(VerifyDigest(strings.NewReader("modpacX"), sumOf("modpack")))
|
|
if !errors.Is(err, ErrDigestMismatch) {
|
|
t.Fatalf("err = %v, want ErrDigestMismatch", err)
|
|
}
|
|
if string(b) != "modpacX" {
|
|
t.Fatalf("passed on %q, want every byte (the store decides what to keep)", b)
|
|
}
|
|
})
|
|
t.Run("a read that breaks off is passed on as it is", func(t *testing.T) {
|
|
_, err := io.ReadAll(VerifyDigest(&failingReader{data: "mod"}, sumOf("mod")))
|
|
if err == nil || errors.Is(err, ErrDigestMismatch) || err.Error() != "connection reset" {
|
|
t.Fatalf("err = %v, want the connection reset itself", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A part that changed on the way is cut back off like one that broke off, below
|
|
// the part cap and exactly at it (where the cap reads one byte past the part).
|
|
func TestChunkedUploadCutsBackAPartChangedOnTheWay(t *testing.T) {
|
|
for _, tc := range []struct{ name, sent, meant string }{
|
|
{"under the part cap", "abX", "abc"},
|
|
{"at the part cap", "abcX", "abcd"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m, _, _, id := newChunkedManager(t)
|
|
sendPart(t, m, id, 0, "\x1f\x8b\x08\x00")
|
|
|
|
_, err := m.UploadPart(context.Background(), id, "user-1", 4, VerifyDigest(strings.NewReader(tc.sent), sumOf(tc.meant)))
|
|
if !errors.Is(err, ErrDigestMismatch) {
|
|
t.Fatalf("err = %v, want ErrDigestMismatch", err)
|
|
}
|
|
if got := staged(t, m, id); got != 4 {
|
|
t.Fatalf("staged after a changed part = %d, want 4", got)
|
|
}
|
|
p, err := m.UploadPart(context.Background(), id, "user-1", 4, VerifyDigest(strings.NewReader(tc.meant), sumOf(tc.meant)))
|
|
if err != nil || p.Received != int64(4+len(tc.meant)) {
|
|
t.Fatalf("the part sent again = (%d, %v), want (%d, nil)", p.Received, err, 4+len(tc.meant))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A context that changed on the way leaves the one before it stored, and its
|
|
// digest recorded.
|
|
func TestUploadContextChangedOnTheWayKeepsThePreviousContext(t *testing.T) {
|
|
m, st, _ := newManager()
|
|
base := t.TempDir()
|
|
m.Blobs = &LocalContextStore{Base: base, MinFree: 1e-9}
|
|
ctx := context.Background()
|
|
seed, _ := m.Create(ctx, CreateRequest{DisplayName: "Pack", SubmittedBy: "user-1"})
|
|
first, second := gzBody("first"), gzBody("second")
|
|
if _, err := m.UploadContext(ctx, seed.ID, "user-1", VerifyDigest(strings.NewReader(first), sumOf(first))); err != nil {
|
|
t.Fatalf("first upload: %v", err)
|
|
}
|
|
|
|
_, err := m.UploadContext(ctx, seed.ID, "user-1", VerifyDigest(strings.NewReader(second+"X"), sumOf(second)))
|
|
|
|
if !errors.Is(err, ErrDigestMismatch) {
|
|
t.Fatalf("err = %v, want ErrDigestMismatch", err)
|
|
}
|
|
if got, _ := os.ReadFile(filepath.Join(base, seed.ID, contextBlobName)); string(got) != first {
|
|
t.Fatalf("stored %q, want the first upload %q", got, first)
|
|
}
|
|
if got := st.subs[seed.ID].ContextSHA256; got != hex.EncodeToString(sumOf(first)) {
|
|
t.Fatalf("recorded digest %s, want the first upload's", got)
|
|
}
|
|
}
|
|
|
|
// Through minio-go, a mismatch found at the end of the stream aborts the
|
|
// multipart upload, whether the last part is short or the stream ends right at a
|
|
// part boundary.
|
|
func TestS3ContextStorePutKeepsNothingChangedOnTheWay(t *testing.T) {
|
|
for _, size := range []int{2*s3PartSize + 5, 2 * s3PartSize} {
|
|
fake := &multipartS3{objects: map[string]string{}}
|
|
ts := httptest.NewServer(fake)
|
|
s, err := NewS3ContextStore(S3StoreConfig{Base: "s3://felis-uploads/builds", Endpoint: ts.URL, Region: "us-east-1", AccessKey: "a", SecretKey: "b"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payload := make([]byte, size)
|
|
rand.New(rand.NewSource(2)).Read(payload)
|
|
meant := sumOf(string(payload))
|
|
payload[size-1] ^= 1
|
|
|
|
_, err = s.Put(context.Background(), "sub-big", VerifyDigest(bytes.NewReader(payload), meant))
|
|
ts.Close()
|
|
|
|
if !errors.Is(err, ErrDigestMismatch) {
|
|
t.Errorf("%d bytes: err = %v, want ErrDigestMismatch", size, err)
|
|
}
|
|
if len(fake.objects) != 0 {
|
|
t.Errorf("%d bytes: an object was completed: %v", size, fake.objects)
|
|
}
|
|
}
|
|
}
|