Take the signature, the expiry and the URL scheme off the image request (closes #314)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The image request now holds only its source and its transform, exactly what the variant key covers. The /v1/image/ handler checks the allowlist and verifies the signature itself with Signer.Verify(req, sig, expires). Both image handlers keep the URL's expiry as a local value for Cache-Control. The image service holds no signer, allowlist or AllowHTTP, and ValidateRequest is gone. The fetcher takes the source and builds its URL with Source.URL from its own AllowHTTP, so no URL string is built and parsed again. chooseAutoFormat returns the format it chooses instead of writing into the request. The signed URL format and every route's answers are unchanged. Model: opus-5-5
This commit is contained in:
@@ -36,7 +36,7 @@ type heldFetcher struct {
|
||||
}
|
||||
|
||||
func (f *heldFetcher) Fetch(
|
||||
ctx context.Context, url string,
|
||||
ctx context.Context, source imagerequest.Source,
|
||||
) (*httpfetcher.FetchResult, error) {
|
||||
f.fetches.Add(1)
|
||||
|
||||
@@ -48,7 +48,7 @@ func (f *heldFetcher) Fetch(
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
return f.upstream.Fetch(ctx, url)
|
||||
return f.upstream.Fetch(ctx, source)
|
||||
}
|
||||
|
||||
// releaseFetches lets every held fetch, and every later one, go on.
|
||||
@@ -489,7 +489,7 @@ func TestService_Get_WaiterGetsProcessingTimedOut(t *testing.T) {
|
||||
type panickingFetcher struct{}
|
||||
|
||||
func (panickingFetcher) Fetch(
|
||||
context.Context, string,
|
||||
context.Context, imagerequest.Source,
|
||||
) (*httpfetcher.FetchResult, error) {
|
||||
panic("upstream fetcher panicked")
|
||||
}
|
||||
|
||||
@@ -7,21 +7,17 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"sneak.berlin/go/pixa/internal/allowlist"
|
||||
"sneak.berlin/go/pixa/internal/httpfetcher"
|
||||
"sneak.berlin/go/pixa/internal/imageprocessor"
|
||||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||||
"sneak.berlin/go/pixa/internal/magic"
|
||||
"sneak.berlin/go/pixa/internal/signature"
|
||||
)
|
||||
|
||||
// Service orchestrates the cache, fetcher, and processor.
|
||||
@@ -29,10 +25,7 @@ type Service struct {
|
||||
cache *Cache
|
||||
fetcher httpfetcher.Fetcher
|
||||
processor *imageprocessor.ImageProcessor
|
||||
signer *signature.Signer
|
||||
allowlist *allowlist.HostAllowList
|
||||
log *slog.Logger
|
||||
allowHTTP bool
|
||||
maxResponseSize int64
|
||||
// variantsInProgress lets the requests that miss the same variant at the
|
||||
// same time share one fetch and one transcode.
|
||||
@@ -47,13 +40,6 @@ type ServiceConfig struct {
|
||||
Fetcher httpfetcher.Fetcher
|
||||
// Processor converts and resizes the images
|
||||
Processor *imageprocessor.ImageProcessor
|
||||
// SigningKey is the HMAC signing key (empty disables signing)
|
||||
SigningKey string
|
||||
// Allowlist is the list of hosts that don't require signatures
|
||||
Allowlist []string
|
||||
// AllowHTTP makes the service fetch sources over http instead of
|
||||
// https (testing only), so the fetcher must allow http too
|
||||
AllowHTTP bool
|
||||
// MaxResponseSize is the largest source accepted, in bytes; a cached
|
||||
// source larger than this is fetched again
|
||||
MaxResponseSize int64
|
||||
@@ -61,11 +47,8 @@ type ServiceConfig struct {
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// Static errors for service construction.
|
||||
var (
|
||||
errCacheRequired = errors.New("cache is required")
|
||||
errSigningKeyRequired = errors.New("signing key is required")
|
||||
)
|
||||
// errCacheRequired is returned when the service is built without a cache.
|
||||
var errCacheRequired = errors.New("cache is required")
|
||||
|
||||
// NewService creates a new image service.
|
||||
func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
@@ -73,10 +56,6 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
return nil, errCacheRequired
|
||||
}
|
||||
|
||||
if cfg.SigningKey == "" {
|
||||
return nil, errSigningKeyRequired
|
||||
}
|
||||
|
||||
log := cfg.Logger
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
@@ -93,10 +72,7 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
cache: cfg.Cache,
|
||||
fetcher: cfg.Fetcher,
|
||||
processor: cfg.Processor,
|
||||
signer: signature.New(cfg.SigningKey),
|
||||
allowlist: allowlist.New(cfg.Allowlist),
|
||||
log: log,
|
||||
allowHTTP: cfg.AllowHTTP,
|
||||
maxResponseSize: cfg.MaxResponseSize,
|
||||
}, nil
|
||||
}
|
||||
@@ -123,9 +99,6 @@ var ErrProcessingTimedOut = errors.New(
|
||||
func (s *Service) Get(
|
||||
ctx context.Context, req *imagerequest.Request,
|
||||
) (*ImageResponse, error) {
|
||||
// Propagate AllowHTTP setting to the request
|
||||
req.AllowHTTP = s.allowHTTP
|
||||
|
||||
cacheKey := imagerequest.CacheKey(req)
|
||||
|
||||
reader, size, contentType, err := s.cache.GetVariant(cacheKey)
|
||||
@@ -169,33 +142,6 @@ func (s *Service) WaitForProcessing(ctx context.Context) int {
|
||||
return s.processor.WaitForProcessing(ctx)
|
||||
}
|
||||
|
||||
// ValidateRequest validates the request signature if required.
|
||||
func (s *Service) ValidateRequest(req *imagerequest.Request) error {
|
||||
// Check if host is allowed (no signature required)
|
||||
sourceURL := req.SourceURL()
|
||||
|
||||
parsedURL, err := url.Parse(sourceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid source URL: %w", err)
|
||||
}
|
||||
|
||||
// An allowlist entry names no port, so a host followed by ":", with or
|
||||
// without a port, needs a signature. The port is read from the parsed
|
||||
// URL, as the fetcher reads it: in an IPv6 address written without
|
||||
// brackets, such as 2001:db8::1:8443, it is what follows the last ":".
|
||||
// This is checked here and not in allowlist.IsAllowed, which also matches
|
||||
// referer_blocklist, where a Referer must be blocked whatever its port.
|
||||
hostHasPort := parsedURL.Port() != "" ||
|
||||
strings.HasSuffix(parsedURL.Host, ":")
|
||||
|
||||
if !hostHasPort && s.allowlist.IsAllowed(parsedURL) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Signature required for non-allowed hosts
|
||||
return s.signer.Verify(req)
|
||||
}
|
||||
|
||||
// errPanicked is returned when processing a variant panicked.
|
||||
var errPanicked = errors.New("panic while processing image")
|
||||
|
||||
@@ -388,11 +334,10 @@ func (s *Service) fetchAndProcess(
|
||||
}
|
||||
|
||||
// Fetch from upstream
|
||||
sourceURL := req.SourceURL()
|
||||
s.log.Debug("fetching from upstream", "host", req.Source.Host,
|
||||
"path", req.Source.Path, "query", req.Source.Query)
|
||||
|
||||
s.log.Debug("fetching from upstream", "url", sourceURL)
|
||||
|
||||
fetchResult, err := s.fetcher.Fetch(ctx, sourceURL)
|
||||
fetchResult, err := s.fetcher.Fetch(ctx, req.Source)
|
||||
if err != nil {
|
||||
// Store negative cache for certain errors
|
||||
if isNegativeCacheable(err) {
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
|
||||
"sneak.berlin/go/pixa/internal/imagerequest"
|
||||
"sneak.berlin/go/pixa/internal/magic"
|
||||
"sneak.berlin/go/pixa/internal/signature"
|
||||
)
|
||||
|
||||
// Test data literals used repeatedly in this file (goconst).
|
||||
const (
|
||||
testPathPhoto = "/images/photo.jpg"
|
||||
testPathUpload = "/uploads/image.jpg"
|
||||
testSigningKey = "test-signing-key-12345"
|
||||
)
|
||||
|
||||
func TestService_Get_AllowlistedHost(t *testing.T) {
|
||||
@@ -60,36 +58,13 @@ func TestService_Get_AllowlistedHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonAllowlistedHost_NoSignature(t *testing.T) {
|
||||
// TestService_Get_NonAllowlistedHost checks that the image service serves a
|
||||
// host that is not on the allowlist as any other: the /v1/image/ handler checks
|
||||
// the signature for such a host before it asks the service for the image.
|
||||
func TestService_Get_NonAllowlistedHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey("test-key"))
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: fixtures.OtherHost,
|
||||
Path: testPathUpload,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Should fail validation - not allowlisted and no signature
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Error("ValidateRequest() expected error for non-allowlisted host without signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonAllowlistedHost_ValidSignature(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
signingKey := testSigningKey
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey(signingKey))
|
||||
svc, fixtures := SetupTestService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
@@ -105,17 +80,6 @@ func TestService_Get_NonAllowlistedHost_ValidSignature(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Generate a valid signature
|
||||
signer := signature.New(signingKey)
|
||||
req.Expires = time.Now().Add(time.Hour)
|
||||
req.Signature = signer.Sign(req)
|
||||
|
||||
// Should pass validation
|
||||
err := svc.ValidateRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateRequest() error = %v", err)
|
||||
}
|
||||
|
||||
// Should fetch successfully
|
||||
resp, err := svc.Get(ctx, req)
|
||||
if err != nil {
|
||||
@@ -134,244 +98,6 @@ func TestService_Get_NonAllowlistedHost_ValidSignature(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonAllowlistedHost_ExpiredSignature(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
signingKey := testSigningKey
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey(signingKey))
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: fixtures.OtherHost,
|
||||
Path: testPathUpload,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Generate an expired signature
|
||||
signer := signature.New(signingKey)
|
||||
req.Expires = time.Now().Add(-time.Hour) // Already expired
|
||||
req.Signature = signer.Sign(req)
|
||||
|
||||
// Should fail validation
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Error("ValidateRequest() expected error for expired signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_NonAllowlistedHost_InvalidSignature(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
signingKey := testSigningKey
|
||||
svc, fixtures := SetupTestService(t, WithSigningKey(signingKey))
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: fixtures.OtherHost,
|
||||
Path: testPathUpload,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
// Set an invalid signature
|
||||
req.Signature = "invalid-signature"
|
||||
req.Expires = time.Now().Add(time.Hour)
|
||||
|
||||
// Should fail validation
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Error("ValidateRequest() expected error for invalid signature")
|
||||
}
|
||||
}
|
||||
|
||||
// TestService_ValidateRequest_SignatureExactHostMatch verifies that
|
||||
// ValidateRequest enforces exact host matching for signatures. A
|
||||
// signature for one host must not verify for a different host, even
|
||||
// if they share a domain suffix.
|
||||
func TestService_ValidateRequest_SignatureExactHostMatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
signingKey := "test-signing-key-must-be-32-chars"
|
||||
svc, _ := SetupTestService(t,
|
||||
WithSigningKey(signingKey),
|
||||
WithNoAllowlist(),
|
||||
)
|
||||
|
||||
signer := signature.New(signingKey)
|
||||
|
||||
// Sign a request for "cdn.example.com"
|
||||
signedReq := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: testHostCDN,
|
||||
Path: testPathCat,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
Expires: time.Now().Add(time.Hour),
|
||||
}
|
||||
signedReq.Signature = signer.Sign(signedReq)
|
||||
|
||||
// The original request should pass validation
|
||||
t.Run("exact host passes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := svc.ValidateRequest(signedReq)
|
||||
if err != nil {
|
||||
t.Errorf("ValidateRequest() exact host failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Try to reuse the signature with different hosts
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
}{
|
||||
{"parent domain", testHostExample},
|
||||
{"sibling subdomain", "images.example.com"},
|
||||
{"deeper subdomain", "a.cdn.example.com"},
|
||||
{"evil suffix domain", "cdn.example.com.evil.com"},
|
||||
{"prefixed host", "evilcdn.example.com"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name+" rejected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: tt.host,
|
||||
Path: signedReq.Source.Path,
|
||||
Query: signedReq.Source.Query,
|
||||
},
|
||||
Transform: signedReq.Transform,
|
||||
Expires: signedReq.Expires,
|
||||
Signature: signedReq.Signature,
|
||||
}
|
||||
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Errorf(
|
||||
"ValidateRequest() should reject signature for host %q (signed for %q)",
|
||||
tt.host, signedReq.Source.Host)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestService_ValidateRequest_HostWithPortNeedsSignature verifies that a host
|
||||
// followed by a port, or by a colon with no port, matches no allowlist entry,
|
||||
// so that a request for it without a signature is refused, while the same
|
||||
// host with no port is allowlisted.
|
||||
func TestService_ValidateRequest_HostWithPortNeedsSignature(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc, _ := SetupTestService(t,
|
||||
WithSigningKey(testSigningKey),
|
||||
WithAllowlist(testHostCDN, "203.0.113.5", "2001:db8::1"),
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
host string
|
||||
allowlisted bool
|
||||
}{
|
||||
{testHostCDN, true},
|
||||
{"cdn.example.com:8443", false},
|
||||
{"cdn.example.com:443", false},
|
||||
{"cdn.example.com:", false},
|
||||
{"203.0.113.5", true},
|
||||
{"203.0.113.5:8080", false},
|
||||
{"[2001:db8::1]", true},
|
||||
{"[2001:db8::1]:8443", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.host, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: tt.host,
|
||||
Path: testPathCat,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.ValidateRequest(req)
|
||||
t.Logf("ValidateRequest() for host %q with no signature: %v",
|
||||
tt.host, err)
|
||||
|
||||
if allowlisted := err == nil; allowlisted != tt.allowlisted {
|
||||
t.Errorf("host %q served without a signature = %v, want %v",
|
||||
tt.host, allowlisted, tt.allowlisted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestService_ValidateRequest_UnbracketedIPv6PortNeedsSignature verifies that
|
||||
// an allowlisted IPv6 address written without brackets and followed by a
|
||||
// port, or by a colon with no port, needs a signature. Go's URL parser, which
|
||||
// the fetcher uses, reads 2001:db8::1:8443 as the host 2001:db8::1 with the
|
||||
// port 8443.
|
||||
func TestService_ValidateRequest_UnbracketedIPv6PortNeedsSignature(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
svc, _ := SetupTestService(t,
|
||||
WithSigningKey(testSigningKey),
|
||||
WithAllowlist("2001:db8::1"),
|
||||
)
|
||||
|
||||
for _, host := range []string{"2001:db8::1:8443", "2001:db8::1:"} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: host,
|
||||
Path: testPathCat,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.ValidateRequest(req)
|
||||
t.Logf("ValidateRequest() for host %q with no signature: %v",
|
||||
host, err)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("host %q served without a signature, want refused",
|
||||
host)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_InvalidFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -692,33 +418,6 @@ func TestService_Get_DifferentSizes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ValidateRequest_NoSigningKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Service with no signing key - all non-allowlisted requests should fail
|
||||
svc, fixtures := SetupTestService(t, WithNoAllowlist())
|
||||
|
||||
req := &imagerequest.Request{
|
||||
Source: imagerequest.Source{
|
||||
Host: fixtures.OtherHost,
|
||||
Path: testPathUpload,
|
||||
},
|
||||
Transform: imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 50, Height: 50},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
Quality: 85,
|
||||
FitMode: imagerequest.FitCover,
|
||||
},
|
||||
}
|
||||
|
||||
err := svc.ValidateRequest(req)
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"ValidateRequest() expected error when no signing key and host not allowlisted",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_Get_ContextCancellation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ type fakeUpstream struct {
|
||||
}
|
||||
|
||||
func (u *fakeUpstream) Fetch(
|
||||
context.Context, string,
|
||||
context.Context, imagerequest.Source,
|
||||
) (*httpfetcher.FetchResult, error) {
|
||||
return &httpfetcher.FetchResult{
|
||||
Content: io.NopCloser(u),
|
||||
|
||||
@@ -161,22 +161,11 @@ func generateTestGIF(t *testing.T, width, height int, c color.Color) []byte {
|
||||
}
|
||||
|
||||
// SetupTestService creates a Service with mock fetcher for testing.
|
||||
func SetupTestService(
|
||||
t *testing.T, opts ...TestServiceOption,
|
||||
) (*Service, *TestFixtures) {
|
||||
func SetupTestService(t *testing.T) (*Service, *TestFixtures) {
|
||||
t.Helper()
|
||||
|
||||
mockFS, fixtures := NewTestFS(t)
|
||||
|
||||
cfg := &testServiceConfig{
|
||||
allowlist: []string{fixtures.GoodHost},
|
||||
signingKey: "test-signing-key-must-be-32-chars",
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(cfg)
|
||||
}
|
||||
|
||||
// Create temp directory for cache
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
@@ -199,8 +188,6 @@ func SetupTestService(
|
||||
MaxInputBytes: 50 << 20,
|
||||
MaxConcurrentProcessing: 2,
|
||||
}),
|
||||
SigningKey: cfg.signingKey,
|
||||
Allowlist: cfg.allowlist,
|
||||
MaxResponseSize: 50 << 20,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -228,32 +215,3 @@ func setupServiceTestDB(t *testing.T) *sql.DB {
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
type testServiceConfig struct {
|
||||
allowlist []string
|
||||
signingKey string
|
||||
}
|
||||
|
||||
// TestServiceOption configures the test service.
|
||||
type TestServiceOption func(*testServiceConfig)
|
||||
|
||||
// WithAllowlist sets the allowlist for the test service.
|
||||
func WithAllowlist(hosts ...string) TestServiceOption {
|
||||
return func(c *testServiceConfig) {
|
||||
c.allowlist = hosts
|
||||
}
|
||||
}
|
||||
|
||||
// WithSigningKey sets the signing key for the test service.
|
||||
func WithSigningKey(key string) TestServiceOption {
|
||||
return func(c *testServiceConfig) {
|
||||
c.signingKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoAllowlist removes all allowlisted hosts.
|
||||
func WithNoAllowlist() TestServiceOption {
|
||||
return func(c *testServiceConfig) {
|
||||
c.allowlist = nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user