package local import ( "context" "io" "mime" "mime/multipart" "net/http/httptest" "strconv" "strings" "testing" "time" "git.yuntuo.hk/k2safe/go-omni/storage" ) func TestSignedPayloadRanges(t *testing.T) { store, err := New(t.TempDir(), 1<<20) if err != nil { t.Fatal(err) } data := "%PDF-1.4\n" + strings.Repeat("0123456789", 100) + "\n%%EOF\n" ctx := context.Background() if _, err = store.Put(ctx, "private/document", strings.NewReader(data), int64(len(data)), "application/pdf"); err != nil { t.Fatal(err) } signer, _ := storage.NewSigner(store, "https://files.example/read", []byte(strings.Repeat("s", 32))) link, err := signer.PresignGetWithOptions(ctx, "private/document", 30*time.Minute, storage.ReadOptions{Disposition: storage.DispositionInline, FileName: "示例.pdf"}) if err != nil { t.Fatal(err) } for _, tc := range []struct { method, span string status int want string }{ {"GET", "", 200, data}, {"HEAD", "", 200, ""}, {"HEAD", "bytes=0-3", 200, ""}, {"GET", "bytes=0-7", 206, data[:8]}, {"GET", "bytes=20-", 206, data[20:]}, {"GET", "bytes=-6", 206, data[len(data)-6:]}, {"GET", "bytes=1000-999999", 206, data[1000:]}, {"GET", "bytes=999999-", 416, ""}, {"GET", "bytes=bad", 416, ""}, } { t.Run(tc.method+tc.span, func(t *testing.T) { r := httptest.NewRequest(tc.method, link.URL, nil) if tc.span != "" { r.Header.Set("Range", tc.span) } w := httptest.NewRecorder() signer.ServeHTTP(w, r) if w.Code != tc.status { t.Fatal(w.Code) } if tc.status != 416 && w.Body.String() != tc.want { t.Fatal("wrong payload slice") } if tc.status == 206 && (w.Header().Get("Content-Length") != strconv.Itoa(len(tc.want)) || !strings.HasSuffix(w.Header().Get("Content-Range"), "/"+strconv.Itoa(len(data)))) { t.Fatal(w.Header()) } if tc.status == 200 && w.Header().Get("Content-Length") != strconv.Itoa(len(data)) { t.Fatal("metadata included in length") } if tc.span == "bytes=999999-" && w.Header().Get("Content-Range") != "bytes */"+strconv.Itoa(len(data)) { t.Fatal(w.Header()) } if w.Header().Get("Cache-Control") != "private, no-store" { t.Fatal("cache policy") } if tc.status != 416 { kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Disposition")) if err != nil || kind != "inline" || params["filename"] != "示例.pdf" || w.Header().Get("Content-Type") != "application/pdf" { t.Fatal(w.Header(), err) } } }) } r := httptest.NewRequest("GET", link.URL, nil) r.Header.Set("Range", "bytes=0-3,10-13") w := httptest.NewRecorder() signer.ServeHTTP(w, r) kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Type")) if err != nil || w.Code != 206 || kind != "multipart/byteranges" { t.Fatal(w.Header(), err) } reader := multipart.NewReader(w.Body, params["boundary"]) for _, want := range []string{data[:4], data[10:14]} { p, err := reader.NextPart() if err != nil { t.Fatal(err) } got, err := io.ReadAll(p) if err != nil || string(got) != want { t.Fatal("multipart bytes", err) } } if _, err := reader.NextPart(); err != io.EOF { t.Fatal(err) } // Random access cannot expose the managed header and still observes context. ctx, cancel := context.WithCancel(ctx) defer cancel() object, err := store.Get(ctx, "private/document") if err != nil { t.Fatal(err) } defer object.Body.Close() seeker := object.Body.(io.ReadSeeker) if size, err := seeker.Seek(0, io.SeekEnd); err != nil || size != int64(len(data)) { t.Fatal(size, err) } if _, err := seeker.Seek(-1, io.SeekStart); err == nil { t.Fatal("seek into header") } if _, err := seeker.Seek(0, io.SeekStart); err != nil { t.Fatal(err) } prefix := make([]byte, 8) if _, err := io.ReadFull(seeker, prefix); err != nil || string(prefix) != data[:8] { t.Fatal("metadata exposed", err) } cancel() if _, err := seeker.Seek(0, io.SeekStart); err != context.Canceled { t.Fatal(err) } if _, err := seeker.Read(prefix); err != context.Canceled { t.Fatal(err) } }