Help us shape the future of Go! Take the 2026 Go Developer Survey

httpfixture

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

HTTP Fixture System

The HTTP fixture system provides a general-purpose mechanism for intercepting HTTP requests and returning predefined responses. This enables testing and development without external dependencies.

Overview

The fixture system is built around a simple interface that allows flexible fixture provision strategies:

type FixtureProvider interface {
    GetFixture(req *http.Request) *Fixture
}

This design allows:

  • Simple matching implementations
  • Script-based dynamic fixture generation
  • Stateful providers that learn from requests
  • Complete control over fixture selection logic

Components

Core Types
Fixture

Defines an HTTP response:

type Fixture struct {
    StatusCode int               // HTTP status code
    Headers    map[string]string // Response headers
    Body       string            // Response body
    Delay      *time.Duration    // Optional delay before responding
}
FixtureProvider

Interface for providing fixtures based on requests:

type FixtureProvider interface {
    GetFixture(req *http.Request) *Fixture
}
Built-in Providers
MapProvider

Simple key-based lookup using "METHOD URL" format:

provider := httpfixture.NewMapProvider(map[string]*httpfixture.Fixture{
    "GET https://api.example.com/data": {
        StatusCode: 200,
        Body:       `{"result": "success"}`,
    },
})
RuleBasedProvider

Matches requests against a set of rules with support for:

  • Exact URL matching
  • Pattern (regex) matching
  • Method matching (including wildcard *)
  • Header matching
rules := []httpfixture.HTTPFixtureRule{
    {
        Request: httpfixture.FixtureRequest{
            Method:  "GET",
            URL:     "https://api.example.com/user/.*",
            URLType: "pattern",
        },
        Response: httpfixture.Fixture{
            StatusCode: 200,
            Body:       `{"user": "any"}`,
        },
    },
}
provider := httpfixture.NewRuleBasedProvider(rules)
FuncProvider

Maximum flexibility - use any function:

provider := httpfixture.NewFuncProvider(func(req *http.Request) *httpfixture.Fixture {
    if strings.HasPrefix(req.URL.Path, "/user/") {
        userID := strings.TrimPrefix(req.URL.Path, "/user/")
        return &httpfixture.Fixture{
            StatusCode: 200,
            Body:       fmt.Sprintf(`{"id": "%s"}`, userID),
        }
    }
    return nil
})
Transport

The Transport implements http.RoundTripper and delegates fixture provision to a FixtureProvider:

transport := httpfixture.NewTransport(httpfixture.TransportConfig{
    Provider: provider,
    Strict:   true,  // Error if no fixture provided
    Fallback: http.DefaultTransport,  // Optional fallback to real HTTP
})

client := &http.Client{Transport: transport}

File-Based Fixtures

Loading from Files

Fixtures can be defined in JSON or YAML files:

// Load from a single file
provider, err := httpfixture.LoadFixturesFromFile("fixtures.yaml")

// Load from all files in a directory
provider, err := httpfixture.LoadFixturesFromDir("fixtures/")
YAML Format
fixtures:
  - request:
      method: GET
      url: https://api.example.com/data
      url_type: exact
    response:
      status: 200
      headers:
        Content-Type: application/json
      body: '{"data": "value"}'

  - request:
      method: GET
      url: https://api.example.com/user/.*
      url_type: pattern
    response:
      status: 200
      body: '{"user": "any"}'
JSON Format
{
  "fixtures": [
    {
      "request": {
        "method": "GET",
        "url": "https://api.example.com/data",
        "url_type": "exact"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "body": "{\"data\": \"value\"}"
      }
    }
  ]
}

Usage with Lua Data Sources

The fixture system integrates seamlessly with Lua data sources:

// Create a fixture provider
provider := httpfixture.NewMapProvider(map[string]*httpfixture.Fixture{
    "GET https://api.example.com/user/alice": {
        StatusCode: 200,
        Body:       `{"username": "alice"}`,
    },
})

// Configure Lua data source with fixtures
ds, err := datasource.NewLuaDataSource(datasource.LuaDataSourceConfig{
    Name:   "user-data",
    Script: script,
    HTTPOptions: []lua.HTTPServiceOption{
        lua.WithTimeout(30 * time.Second),
        lua.WithTransport(httpfixture.NewTransport(httpfixture.TransportConfig{
            Provider: provider,
            Strict:   true,
        })),
    },
})

Testing Examples

Basic Test with Fixtures
func TestMyDataSource(t *testing.T) {
    provider := httpfixture.NewMapProvider(map[string]*httpfixture.Fixture{
        "GET https://api.example.com/data": {
            StatusCode: 200,
            Body:       `{"test": "data"}`,
        },
    })

    ds := setupDataSource(provider)
    result, err := ds.Fetch(context.Background(), input)
    
    // Assertions...
}
Dynamic Fixtures
func TestWithDynamicFixtures(t *testing.T) {
    callCount := 0
    provider := httpfixture.NewFuncProvider(func(req *http.Request) *httpfixture.Fixture {
        callCount++
        return &httpfixture.Fixture{
            StatusCode: 200,
            Body:       fmt.Sprintf(`{"call": %d}`, callCount),
        }
    })

    // Test...
}
File-Based Fixtures for Integration Tests
func TestIntegration(t *testing.T) {
    provider, err := httpfixture.LoadFixturesFromFile("testdata/api_fixtures.yaml")
    if err != nil {
        t.Fatal(err)
    }

    ds := setupDataSource(provider)
    // Run integration test scenarios...
}

Best Practices

  1. Use Fixtures for All Tests: Avoid real HTTP calls in tests for speed and reliability
  2. Organize Fixtures: Group related fixtures in separate files
  3. Be Specific: Match exact URLs when possible, use patterns sparingly
  4. Order Matters: In rule-based providers, place specific rules before generic ones
  5. Test Fixtures: Verify your fixtures accurately represent real API responses
  6. Document Fixtures: Add comments explaining complex patterns or edge cases
  7. Version Control: Commit fixture files alongside tests

Advanced Features

Response Delays

Simulate network latency:

delay := 100 * time.Millisecond
fixture := &httpfixture.Fixture{
    StatusCode: 200,
    Body:       "slow response",
    Delay:      &delay,
}
Header Matching

Match requests based on headers:

rule := httpfixture.HTTPFixtureRule{
    Request: httpfixture.FixtureRequest{
        Method: "GET",
        URL:    "https://api.example.com/secure",
        Headers: map[string]string{
            "Authorization": "Bearer token123",
        },
    },
    Response: httpfixture.Fixture{
        StatusCode: 200,
        Body:       `{"authenticated": true}`,
    },
}
Fallback to Real HTTP

For partial mocking scenarios:

transport := httpfixture.NewTransport(httpfixture.TransportConfig{
    Provider: provider,
    Fallback: http.DefaultTransport,
    Strict:   false,  // Don't error on missing fixtures
})

Architecture

The fixture system is designed to be:

  1. General-Purpose: Not tied to Lua or any specific use case
  2. Flexible: Provider interface allows any fixture selection strategy
  3. Composable: Can be used with any http.Client
  4. Testable: Makes tests fast, deterministic, and hermetic
  5. Extensible: Easy to add custom providers or matching logic

Dependencies

The package uses github.com/goccy/go-yaml for YAML parsing, which is an actively maintained pure Go YAML 1.2 implementation with excellent error reporting and performance.

JWKS Fixtures

Overview

JWKS (JSON Web Key Set) fixtures are specialized fixtures for testing JWT validators without external dependencies. They provide both HTTP fixture responses for JWKS endpoints and a convenient Go API for signing test tokens.

Key Features
  • Automatic Key Generation: Generates RSA key pairs on creation
  • FixtureProvider Implementation: Serves JWKS responses via HTTP fixture interface
  • Signing API: Convenient methods for creating and signing test tokens
  • Time Control: Optional clock injection for precise control over token timestamps
  • Hermetic Testing: No need for httptest servers or external services
Creating a JWKS Fixture
fixture, err := httpfixture.NewJWKSFixture(httpfixture.JWKSFixtureConfig{
    Issuer:  "https://test-issuer.example.com",
    JWKSURL: "https://test-issuer.example.com/.well-known/jwks.json",
    KeyID:   "test-key-1",  // Optional, defaults to "test-key-1"
    Algorithm: jwa.RS256,    // Optional, defaults to RS256
})
if err != nil {
    t.Fatal(err)
}
Using with JWT Validators
// Create HTTP client with fixture transport
httpClient := &http.Client{
    Transport: httpfixture.NewTransport(httpfixture.TransportConfig{
        Provider: fixture,
        Strict:   true,
    }),
}

// Create validator with fixture client
validator, err := trust.NewJWTValidator(trust.JWTValidatorConfig{
    Issuer:      fixture.Issuer(),
    JWKSURL:     fixture.JWKSURL(),
    TrustDomain: "test-domain",
    HTTPClient:  httpClient,
})

// Create and sign test tokens
tokenString, err := fixture.CreateAndSignToken(map[string]interface{}{
    "sub":   "user@example.com",
    "email": "user@example.com",
    "roles": []string{"admin", "user"},
})
Signing API
CreateAndSignToken

Creates a JWT with standard claims (iss, iat, exp) automatically set, plus custom claims:

tokenString, err := fixture.CreateAndSignToken(map[string]interface{}{
    "sub":    "user-123",
    "email":  "user@example.com",
    "custom": "value",
})
CreateAndSignTokenWithExpiry

Creates a JWT with custom expiry time (useful for testing expiration):

expiry := time.Now().Add(-1 * time.Hour) // Expired 1 hour ago
tokenString, err := fixture.CreateAndSignTokenWithExpiry(
    map[string]interface{}{"sub": "user@example.com"},
    expiry,
)
SignToken

Signs a pre-built JWT token for maximum control:

token := jwt.New()
token.Set(jwt.SubjectKey, "custom-subject")
token.Set("custom_claim", "custom_value")

tokenString, err := fixture.SignToken(token)
Testing Multiple Issuers

Create multiple fixtures with different issuers to test authorization scenarios:

prodFixture, _ := httpfixture.NewJWKSFixture(httpfixture.JWKSFixtureConfig{
    Issuer:  "https://prod-issuer.example.com",
    JWKSURL: "https://prod-issuer.example.com/.well-known/jwks.json",
})

testFixture, _ := httpfixture.NewJWKSFixture(httpfixture.JWKSFixtureConfig{
    Issuer:  "https://test-issuer.example.com",
    JWKSURL: "https://test-issuer.example.com/.well-known/jwks.json",
})

// Create composite provider for both fixtures
compositeProvider := httpfixture.NewFuncProvider(func(req *http.Request) *httpfixture.Fixture {
    if f := prodFixture.GetFixture(req); f != nil {
        return f
    }
    return testFixture.GetFixture(req)
})
Time Control with Clock Fixtures

Use clock.FixtureClock for precise control over token timestamps and expiration testing:

import "github.com/project-kessel/parsec/internal/clock"

func TestTokenExpiration(t *testing.T) {
    // Create a fixture clock at a specific time
    fixedTime := time.Date(2024, 6, 15, 10, 0, 0, 0, time.UTC)
    clk := clock.NewFixtureClock(fixedTime)

    // Create JWKS fixture with controlled clock
    fixture, _ := httpfixture.NewJWKSFixture(httpfixture.JWKSFixtureConfig{
        Issuer:  "https://auth.example.com",
        JWKSURL: "https://auth.example.com/.well-known/jwks.json",
        Clock:   clk,
    })

    // Create validator with same clock for consistent time behavior
    httpClient := &http.Client{
        Transport: httpfixture.NewTransport(httpfixture.TransportConfig{
            Provider: fixture,
            Strict:   true,
        }),
    }

    validator, _ := trust.NewJWTValidator(trust.JWTValidatorConfig{
        Issuer:      fixture.Issuer(),
        JWKSURL:     fixture.JWKSURL(),
        TrustDomain: "test-domain",
        HTTPClient:  httpClient,
        Clock:       fixture.Clock(), // Same clock for validator
    })

    // Create token valid for 1 hour
    tokenString, _ := fixture.CreateAndSignToken(map[string]interface{}{
        "sub": "user@example.com",
    })

    cred := &trust.JWTCredential{
        BearerCredential: trust.BearerCredential{Token: tokenString},
    }

    // Token is valid now
    _, err := validator.Validate(context.Background(), cred)
    if err != nil {
        t.Errorf("expected token to be valid: %v", err)
    }

    // Advance clock by 30 minutes - still valid
    clk.Advance(30 * time.Minute)
    _, err = validator.Validate(context.Background(), cred)
    if err != nil {
        t.Errorf("expected token to still be valid: %v", err)
    }

    // Advance clock by 31 more minutes - now expired
    clk.Advance(31 * time.Minute)
    _, err = validator.Validate(context.Background(), cred)
    if err == nil {
        t.Error("expected token to be expired")
    }
}
Complete Example
func TestJWTValidation(t *testing.T) {
    // Setup fixture
    fixture, err := httpfixture.NewJWKSFixture(httpfixture.JWKSFixtureConfig{
        Issuer:  "https://auth.example.com",
        JWKSURL: "https://auth.example.com/.well-known/jwks.json",
    })
    if err != nil {
        t.Fatal(err)
    }

    // Create validator with fixture
    httpClient := &http.Client{
        Transport: httpfixture.NewTransport(httpfixture.TransportConfig{
            Provider: fixture,
            Strict:   true,
        }),
    }

    validator, err := trust.NewJWTValidator(trust.JWTValidatorConfig{
        Issuer:      fixture.Issuer(),
        JWKSURL:     fixture.JWKSURL(),
        TrustDomain: "test-domain",
        HTTPClient:  httpClient,
        Clock:       fixture.Clock(), // Use fixture's clock
    })
    if err != nil {
        t.Fatal(err)
    }

    // Test with valid token
    tokenString, err := fixture.CreateAndSignToken(map[string]interface{}{
        "sub":   "user@example.com",
        "role":  "admin",
    })
    if err != nil {
        t.Fatal(err)
    }

    cred := &trust.JWTCredential{
        BearerCredential: trust.BearerCredential{Token: tokenString},
    }

    result, err := validator.Validate(context.Background(), cred)
    if err != nil {
        t.Fatalf("validation failed: %v", err)
    }

    // Assert on result
    if result.Subject != "user@example.com" {
        t.Errorf("unexpected subject: %v", result.Subject)
    }
}

Package Structure

internal/httpfixture/
├── fixture.go            # Core types and interfaces
├── providers.go          # Built-in provider implementations
├── transport.go          # HTTP RoundTripper implementation
├── loader.go             # File loading utilities
├── jwks_fixture.go       # JWKS fixture for JWT testing
├── fixture_test.go       # Tests
├── jwks_fixture_test.go  # JWKS fixture tests
└── README.md             # This file

Future Enhancements

Potential future additions:

  • Request body matching
  • Response templating with request data
  • Recording mode (capture real responses as fixtures)
  • Fixture validation against OpenAPI specs
  • HTTP/2 support

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type CompositeFixtureProvider

type CompositeFixtureProvider struct {
	// contains filtered or unexported fields
}

CompositeFixtureProvider composes multiple fixture providers and provides direct access to specific fixture types (e.g., JWKS fixtures by issuer)

func NewCompositeFixtureProvider

func NewCompositeFixtureProvider(providers []FixtureProvider, jwksFixtures map[string]*JWKSFixture) *CompositeFixtureProvider

NewCompositeFixtureProvider creates a composite fixture provider

func (*CompositeFixtureProvider) GetFixture

func (c *CompositeFixtureProvider) GetFixture(req *http.Request) *Fixture

GetFixture implements FixtureProvider by trying each provider in order

func (*CompositeFixtureProvider) JWKSFixture

func (c *CompositeFixtureProvider) JWKSFixture(issuer string) *JWKSFixture

JWKSFixture returns a JWKS fixture by issuer, or nil if not found

type Fixture

type Fixture struct {
	StatusCode int               `json:"status" yaml:"status"`
	Headers    map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
	Body       string            `json:"body" yaml:"body"`
	Delay      *time.Duration    `json:"delay,omitempty" yaml:"delay,omitempty"`
}

Fixture defines an HTTP response to return for requests

type FixtureProvider

type FixtureProvider interface {
	GetFixture(req *http.Request) *Fixture
}

FixtureProvider returns a fixture for a request, or nil if no fixture applies

type FixtureRequest

type FixtureRequest struct {
	Method  string            `json:"method" yaml:"method"`                         // e.g., "GET", "POST", "*" for any
	URL     string            `json:"url" yaml:"url"`                               // exact match or pattern
	URLType string            `json:"url_type,omitempty" yaml:"url_type,omitempty"` // "exact" (default) or "pattern"
	Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`   // optional header matching
}

FixtureRequest defines request matching criteria (for file-based fixtures)

type FixtureSet

type FixtureSet struct {
	Rules []HTTPFixtureRule `json:"fixtures" yaml:"fixtures"`
}

FixtureSet is a collection of fixture rules (for file loading)

type FuncProvider

type FuncProvider struct {
	// contains filtered or unexported fields
}

FuncProvider uses a function to provide fixtures (most flexible)

func NewFuncProvider

func NewFuncProvider(fn func(*http.Request) *Fixture) *FuncProvider

NewFuncProvider creates a new function-based fixture provider

func (*FuncProvider) GetFixture

func (p *FuncProvider) GetFixture(req *http.Request) *Fixture

GetFixture returns a fixture by calling the provided function

type HTTPFixtureRule

type HTTPFixtureRule struct {
	Request  FixtureRequest `json:"request" yaml:"request"`
	Response Fixture        `json:"response" yaml:"response"`
}

HTTPFixtureRule defines request criteria and corresponding response (for file-based fixtures)

type JWKSFixture

type JWKSFixture struct {
	// contains filtered or unexported fields
}

JWKSFixture is a specialized HTTP fixture that serves a JWKS endpoint and provides a signing API for creating test tokens with corresponding private keys

func NewJWKSFixture

func NewJWKSFixture(cfg JWKSFixtureConfig) (*JWKSFixture, error)

NewJWKSFixture creates a new JWKS fixture with a generated RSA key pair

func (*JWKSFixture) Clock

func (f *JWKSFixture) Clock() clock.Clock

Clock returns the clock used by this fixture

func (*JWKSFixture) CreateAndSignToken

func (f *JWKSFixture) CreateAndSignToken(claims map[string]interface{}) (string, error)

CreateAndSignToken creates a new JWT with the given claims and signs it The issuer, issued-at, and expiration claims are set automatically using the fixture's clock

func (*JWKSFixture) CreateAndSignTokenWithExpiry

func (f *JWKSFixture) CreateAndSignTokenWithExpiry(claims map[string]interface{}, expiry time.Time) (string, error)

CreateAndSignTokenWithExpiry creates a new JWT with the given claims and custom expiry, and signs it Uses the fixture's clock for the issued-at claim

func (*JWKSFixture) GetFixture

func (f *JWKSFixture) GetFixture(req *http.Request) *Fixture

GetFixture implements FixtureProvider interface Returns a fixture for JWKS URL requests

func (*JWKSFixture) Issuer

func (f *JWKSFixture) Issuer() string

Issuer returns the issuer URL

func (*JWKSFixture) JWKSURL

func (f *JWKSFixture) JWKSURL() string

JWKSURL returns the JWKS URL this fixture serves

func (*JWKSFixture) KeyID

func (f *JWKSFixture) KeyID() string

KeyID returns the key identifier

func (*JWKSFixture) SignToken

func (f *JWKSFixture) SignToken(token jwt.Token) (string, error)

SignToken signs a JWT token using the fixture's private key

type JWKSFixtureConfig

type JWKSFixtureConfig struct {
	// Issuer is the issuer URL (for iss claim)
	Issuer string

	// JWKSURL is the URL where the JWKS will be served
	JWKSURL string

	// KeyID is the key identifier (kid)
	// If empty, defaults to "test-key-1"
	KeyID string

	// Algorithm is the signing algorithm
	// If zero value, defaults to RS256
	Algorithm jwa.SignatureAlgorithm

	// Clock is the time source for token timestamps
	// If nil, uses system clock
	Clock clock.Clock

	// PrivateKeyPEM is optional PEM-encoded RSA private key material.
	// When set, the fixture uses this key instead of generating a new one.
	PrivateKeyPEM []byte

	// PrivateKeyFile is a path to a PEM-encoded RSA private key file.
	PrivateKeyFile string
}

JWKSFixtureConfig configures a JWKS fixture

type MapProvider

type MapProvider struct {
	// contains filtered or unexported fields
}

MapProvider provides fixtures based on a simple map lookup (method+URL key)

func NewMapProvider

func NewMapProvider(fixtures map[string]*Fixture) *MapProvider

NewMapProvider creates a new map-based fixture provider Key format: "METHOD URL" (e.g., "GET https://api.example.com/user/alice")

func (*MapProvider) GetFixture

func (p *MapProvider) GetFixture(req *http.Request) *Fixture

GetFixture returns a fixture for the given request based on method+URL key

type RuleBasedProvider

type RuleBasedProvider struct {
	// contains filtered or unexported fields
}

RuleBasedProvider matches requests against a set of rules

func LoadFixturesFromDir

func LoadFixturesFromDir(dir string) (*RuleBasedProvider, error)

LoadFixturesFromDir loads all fixture files from a directory

func LoadFixturesFromFile

func LoadFixturesFromFile(path string) (*RuleBasedProvider, error)

LoadFixturesFromFile loads fixtures from a JSON or YAML file

func NewRuleBasedProvider

func NewRuleBasedProvider(rules []HTTPFixtureRule) *RuleBasedProvider

NewRuleBasedProvider creates a new rule-based fixture provider

func (*RuleBasedProvider) GetFixture

func (p *RuleBasedProvider) GetFixture(req *http.Request) *Fixture

GetFixture returns a fixture for the given request if any rule matches

type Transport

type Transport struct {
	// contains filtered or unexported fields
}

Transport implements http.RoundTripper using a FixtureProvider

func NewTransport

func NewTransport(config TransportConfig) *Transport

NewTransport creates a new fixture transport

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper

type TransportConfig

type TransportConfig struct {
	Provider FixtureProvider
	Fallback http.RoundTripper // optional fallback transport
	Strict   bool              // if true, error when provider returns nil
	Clock    clock.Clock       // optional clock for delays (defaults to system clock)
}

TransportConfig configures the fixture transport

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL