Luke Granger-Brown
5b74ceec4e
This allows clients to register an authentication middleware which will be provided values in the context that it can use to make an authn/authz decision.
305 lines
8.1 KiB
Go
305 lines
8.1 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"
|
|
"strings"
|
|
"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)
|
|
}
|
|
if err := tc.backend.WriteAll(ctx, "expired.txt", []byte("hello world\n"), &blob.WriterOptions{
|
|
Metadata: map[string]string{
|
|
// This unix timestamp is very much in the past.
|
|
"expires-at": "1000",
|
|
},
|
|
}); 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)
|
|
}
|
|
})
|
|
|
|
t.Run("expired file", func(t *testing.T) {
|
|
// Verify the file is there.
|
|
if ok, err := tc.backend.Exists(ctx, "expired.txt"); err != nil {
|
|
t.Fatalf("Exists(%q) before test: %v", "expired.txt", err)
|
|
} else if !ok {
|
|
t.Fatalf("Exists(%q) before test returned false", "expired.txt")
|
|
}
|
|
|
|
resp, err := s.Client().Get(fmt.Sprintf("%s/raw/expired.txt", 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)
|
|
}
|
|
|
|
// Verify the file is NOT there.
|
|
if ok, err := tc.backend.Exists(ctx, "expired.txt"); err != nil {
|
|
t.Fatalf("Exists(%q) after retrieving: %v", "expired.txt", err)
|
|
} else if ok {
|
|
t.Fatalf("Exists(%q) after retrieving returned true", "expired.txt")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
|
|
var gotReq, lastReqWasMutate, lastReqWasAPI bool
|
|
|
|
ccfg := *cfg
|
|
ccfg.AuthMiddleware = func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
gotReq = true
|
|
lastReqWasMutate = fuphttp.IsMutate(r.Context())
|
|
lastReqWasAPI = fuphttp.IsAPIRequest(r.Context())
|
|
next.ServeHTTP(rw, r)
|
|
})
|
|
}
|
|
|
|
a, err := fuphttp.New(ctx, &ccfg)
|
|
if err != nil {
|
|
t.Fatalf("fuphttp.New: %v", err)
|
|
}
|
|
s := httptest.NewServer(a.Handler())
|
|
t.Cleanup(s.Close)
|
|
|
|
tcs := []struct {
|
|
name string
|
|
request func(url string) (*http.Request, error)
|
|
wantIsMutate bool
|
|
wantIsAPI bool
|
|
}{{
|
|
name: "/ request",
|
|
request: func(url string) (*http.Request, error) { return http.NewRequest("GET", url, nil) },
|
|
wantIsMutate: true,
|
|
wantIsAPI: false,
|
|
}, {
|
|
name: "/upload request",
|
|
request: func(url string) (*http.Request, error) {
|
|
return http.NewRequest("PUT", url+"/upload/foo.txt", strings.NewReader("slartibartfast\n"))
|
|
},
|
|
wantIsMutate: true,
|
|
wantIsAPI: false,
|
|
}, {
|
|
name: "/upload request, application/json",
|
|
request: func(url string) (*http.Request, error) {
|
|
req, err := http.NewRequest("PUT", url+"/upload/foo.txt", strings.NewReader("slartibartfast\n"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Accept", "application/json")
|
|
return req, nil
|
|
},
|
|
wantIsMutate: true,
|
|
wantIsAPI: true,
|
|
}, {
|
|
name: "/foo.txt request",
|
|
request: func(url string) (*http.Request, error) { return http.NewRequest("GET", url+"/foo.txt", nil) },
|
|
wantIsMutate: false,
|
|
wantIsAPI: false,
|
|
}, {
|
|
name: "/raw/foo.txt request",
|
|
request: func(url string) (*http.Request, error) { return http.NewRequest("GET", url+"/raw/foo.txt", nil) },
|
|
wantIsMutate: false,
|
|
wantIsAPI: false,
|
|
}}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotReq = false
|
|
|
|
req, err := tc.request(s.URL)
|
|
resp, err := s.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatalf("Do: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if !gotReq {
|
|
t.Fatalf("gotReq = %v; want true", gotReq)
|
|
}
|
|
if lastReqWasMutate != tc.wantIsMutate {
|
|
t.Errorf("lastReqWasMutate = %v; want %v", lastReqWasMutate, tc.wantIsMutate)
|
|
}
|
|
if lastReqWasAPI != tc.wantIsAPI {
|
|
t.Errorf("lastReqWasAPI = %v; want %v", lastReqWasAPI, tc.wantIsAPI)
|
|
}
|
|
})
|
|
}
|
|
}
|