depot/web/fup/fuphttp/fuphttp_test.go
Luke Granger-Brown 0e69199569 fup: shore up serving and add tests for supported methods
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.
2021-03-21 14:35:04 +00:00

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)
}
})
})
}
}