Luke Granger-Brown
0e69199569
We support several methods of serving assets: * Redirect to blobstore - This requires the backend to support signed URLs. We rely on the backend to support HTTP semantics, like supporting Range headers. * Serve-using-http.ServeContent - This requires the backend to actually be providing a io.ReadSeeker. net/http provides Range/If- conditional support. * Serve-proxy - This is the safest and most compatible method. We don't support conditionals nor Range headers. This mode is unlikely to be suitable for multimedia, like MP3s or video.
182 lines
4.5 KiB
Go
182 lines
4.5 KiB
Go
// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package fuphttp_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"hg.lukegb.com/lukegb/depot/web/fup/fuphttp"
|
|
"hg.lukegb.com/lukegb/depot/web/fup/fupstatic"
|
|
|
|
"gocloud.dev/blob"
|
|
"gocloud.dev/blob/fileblob"
|
|
"gocloud.dev/blob/memblob"
|
|
)
|
|
|
|
var cfg = &fuphttp.Config{
|
|
Templates: fupstatic.Templates,
|
|
StorageURL: "mem://",
|
|
}
|
|
|
|
func TestNotFound(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
a, err := fuphttp.New(ctx, cfg)
|
|
if err != nil {
|
|
t.Fatalf("fuphttp.New: %v", err)
|
|
}
|
|
s := httptest.NewServer(a.Handler())
|
|
defer s.Close()
|
|
|
|
resp, err := s.Client().Get(fmt.Sprintf("%s/not-found", s.URL))
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("response status was %v; want %v", resp.StatusCode, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func mustURL(s string) *url.URL {
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("parsing %q: %v", s, err))
|
|
}
|
|
return u
|
|
}
|
|
|
|
func mustBucket(b *blob.Bucket, err error) *blob.Bucket {
|
|
if err != nil {
|
|
panic(fmt.Sprintf("opening bucket: %v", err))
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestRetrieve(t *testing.T) {
|
|
signer := fileblob.NewURLSignerHMAC(mustURL("http://example.com"), []byte("not secret"))
|
|
tcs := []struct {
|
|
name string
|
|
backend *blob.Bucket
|
|
wantSignedRedirect string
|
|
supportsRange bool
|
|
}{{
|
|
name: "LocalServe",
|
|
backend: memblob.OpenBucket(nil),
|
|
supportsRange: false,
|
|
}, {
|
|
name: "ReadSeeker",
|
|
backend: mustBucket(fileblob.OpenBucket(t.TempDir(), nil)),
|
|
supportsRange: true,
|
|
}, {
|
|
name: "SignedURL",
|
|
backend: mustBucket(fileblob.OpenBucket(t.TempDir(), &fileblob.Options{
|
|
URLSigner: signer,
|
|
})),
|
|
wantSignedRedirect: "http://example.com",
|
|
}}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
|
|
ccfg := *cfg
|
|
ccfg.StorageURL = ""
|
|
ccfg.StorageBackend = tc.backend
|
|
ccfg.RedirectToBlobstore = true
|
|
|
|
if err := tc.backend.WriteAll(ctx, "hello.txt", []byte("hello world\n"), nil); err != nil {
|
|
t.Fatalf("WriteAll: %v", err)
|
|
}
|
|
|
|
a, err := fuphttp.New(ctx, &ccfg)
|
|
if err != nil {
|
|
t.Fatalf("fuphttp.New: %v", err)
|
|
}
|
|
s := httptest.NewServer(a.Handler())
|
|
defer s.Close()
|
|
|
|
client := s.Client()
|
|
client.CheckRedirect = func(r *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}
|
|
|
|
t.Run("get all", func(t *testing.T) {
|
|
resp, err := s.Client().Get(fmt.Sprintf("%s/raw/hello.txt", s.URL))
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if tc.wantSignedRedirect != "" {
|
|
if resp.StatusCode != http.StatusFound {
|
|
t.Errorf("response status was %v; want %v", resp.StatusCode, http.StatusFound)
|
|
}
|
|
|
|
gotLoc := resp.Header.Get("Location")
|
|
gotLocURL, err := url.Parse(gotLoc)
|
|
if err != nil {
|
|
t.Errorf("parsing Location %q: %v", gotLoc, err)
|
|
} else {
|
|
obj, err := signer.KeyFromURL(ctx, gotLocURL)
|
|
if err != nil {
|
|
t.Errorf("KeyFromURL(%q): %v", gotLoc, err)
|
|
} else if obj != "hello.txt" {
|
|
t.Errorf("KeyFromURL(%q) = %v; want %v", gotLoc, obj, "hello.txt")
|
|
}
|
|
}
|
|
} else {
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("response status was %v; want %v", resp.StatusCode, http.StatusOK)
|
|
}
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Errorf("ReadAll: %v", err)
|
|
}
|
|
if got, want := string(data), "hello world\n"; got != want {
|
|
t.Errorf("read data was %q; want %q", got, want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("get range", func(t *testing.T) {
|
|
if !tc.supportsRange {
|
|
t.Skip("range unsupported by this backend")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/raw/hello.txt", s.URL), nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRequestWithContext: %v", err)
|
|
}
|
|
req.Header.Set("Range", "bytes=1-3")
|
|
|
|
resp, err := s.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Client.Do: %v", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusPartialContent {
|
|
t.Fatalf("response status was %v; want %v", resp.StatusCode, http.StatusPartialContent)
|
|
}
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Errorf("ReadAll: %v", err)
|
|
}
|
|
if got, want := string(data), "ell"; got != want {
|
|
t.Errorf("read data was %q; want %q", got, want)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|