package storagetest import ( "context" "io" "net/http" "strconv" "strings" "testing" "time" "git.yuntuo.hk/k2safe/go-omni/storage" ) type UploadStore interface { storage.Store storage.UploadPresigner storage.MultipartStore storage.PartPresigner } func sendUpload(t *testing.T, client *http.Client, link storage.UploadRequest, body string) *http.Response { t.Helper() req, err := http.NewRequest(link.Method, link.URL, strings.NewReader(body)) if err != nil { t.Fatal(err) } req.Header = link.Headers.Clone() resp, err := client.Do(req) if err != nil { t.Fatal(err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) resp.Body.Close() t.Fatalf("upload rejected: %d %s", resp.StatusCode, raw) } return resp } // RunUploads exercises both SDK upload and direct HTTP requests against a // caller-owned server with maxBytes >=6 MiB. The caller cleans its bucket. func RunUploads(t *testing.T, s UploadStore, client *http.Client) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() link, err := s.PresignPut(ctx, "direct", 5, "text/plain", time.Minute) if err != nil { t.Fatal(err) } if link.Method != "PUT" || link.Headers.Get("Content-Length") != "5" || link.Headers.Get("Content-Type") != "text/plain" { t.Fatal("missing constraints", link.Headers) } resp := sendUpload(t, client, link, "hello") resp.Body.Close() defer s.Delete(context.Background(), "direct") object, err := s.Get(ctx, "direct") if err != nil { t.Fatal(err) } raw, _ := io.ReadAll(object.Body) object.Body.Close() if string(raw) != "hello" { t.Fatal("bad direct body") } if _, err = s.PresignPut(ctx, "direct", -1, "text/plain", time.Minute); err == nil { t.Fatal("negative size accepted") } if _, err = s.PresignPut(ctx, "direct", 1, "text/plain", 2*time.Hour); err == nil { t.Fatal("long TTL accepted") } upload, err := s.CreateMultipart(ctx, "multipart", "text/plain") if err != nil { t.Fatal(err) } defer s.AbortMultipart(context.Background(), upload) data := strings.Repeat("a", 5<<20) first, err := s.UploadPart(ctx, upload, 1, strings.NewReader(data), int64(len(data))) if err != nil { t.Fatal(err) } link, err = s.PresignPart(ctx, upload, 2, 5, time.Minute) if err != nil { t.Fatal(err) } resp = sendUpload(t, client, link, "world") etag := resp.Header.Get("ETag") resp.Body.Close() if etag == "" { t.Fatal("no part ETag") } page, err := s.ListParts(ctx, upload, 0, 1) if err != nil || len(page.Parts) != 1 || page.NextMarker != 1 { t.Fatal(page, err) } next, err := s.ListParts(ctx, upload, page.NextMarker, 1) if err != nil || len(next.Parts) != 1 || next.Parts[0].Number != 2 || next.Parts[0].Size != 5 || next.NextMarker != 0 { t.Fatal(next, err) } first.Size = 1 // The provider's size must prevail over caller metadata. info, err := s.CompleteMultipart(ctx, upload, []storage.Part{first, {Number: 2, ETag: etag, Size: 1}}) if err != nil || info.Size != int64(len(data)+5) { t.Fatal(info, err) } defer s.Delete(context.Background(), "multipart") object, err = s.Get(ctx, "multipart") if err != nil { t.Fatal(err) } raw, err = io.ReadAll(object.Body) object.Body.Close() if err != nil || string(raw) != data+"world" { t.Fatal("multipart bytes mismatch", len(raw), err) } canceled, err := s.CreateMultipart(ctx, "canceled", "text/plain") if err != nil { t.Fatal(err) } for i := 0; i < 2; i++ { if err = s.AbortMultipart(ctx, canceled); err != nil { t.Fatal("abort "+strconv.Itoa(i), err) } } if _, err = s.ListParts(ctx, canceled, 0, 1); err == nil { t.Fatal("aborted upload remains") } }