Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

goa-vcr

github.com/xeger/goa-vcr provides:

  • runtime/: transport-agnostic VCR primitives (policy, stub store, route matching, stub doer, recording transport).
  • plugin/vcr/: a Goa v3 codegen plugin that generates per-service glue into gen/http/<service>/vcr.

Try it on a service (minimal)

goa-vcr currently targets Goa v3.23.x; v3.25+ compatibility is deferred.

The Goa plugin model is: your plugin must be linked into the generator binary that runs during goa gen (plugin guide).

  1. Add a blank import in your design module (any file in the design package):
import _ "github.com/xeger/goa-vcr/plugin/vcr"
  1. Run generation:
# IMPORTANT: your `goa` CLI must be compatible with the `goa.design/goa/v3` module
# version used by your repo. If you see a compile error like:
#   "not enough arguments in call to generator.Generate"
# it means your installed `goa` binary is older/newer than the module.
#
# This repo pins goa to v3.23.4, so this is a safe invocation:
go run goa.design/goa/v3/cmd/goa@v3.23.4 gen <design-import-path> -o .

Scenarios as Service layers

The generated per-service Scenario implements your Goa Service interface. A scenario holds a per-method handler queue plus a reference to the Service "below" it (another Scenario, or the stub-backed background). Calls to a method dispatch to the queued handler if one is set; otherwise they delegate to the layer below. Handlers for unary methods receive the layer below as a next argument so they can call through and post-process results.

import (
    toy "<your-module>/gen/toy"
    toyvcr "<your-module>/gen/http/toy/vcr"
    vcrruntime "github.com/xeger/goa-vcr/runtime"
)

// Background: stub-backed Service.
bg := toyvcr.NewBackground(store)

// Single scenario: override one method, delegate the rest to bg.
sc := toyvcr.NewScenario(bg)
sc.SetGetThing(func(ctx context.Context, p *toy.GetThingPayload, next toy.Service) (*toy.Thing, error) {
    t, err := next.GetThing(ctx, p)
    if err != nil { return nil, err }
    t.Name = "patched"
    return t, nil
})

handler, _ := toyvcr.NewPlaybackHandler(sc)

Stacking scenarios

Stack(bg, outer, middle, inner) composes layers with the first argument outermost (outer(middle(inner(bg)))). Layers are ordinary middleware-style functions func(next Service) Service:

func addLogging(next toy.Service) toy.Service {
    s := toyvcr.NewScenario(next)
    s.SetListThings(func(ctx context.Context, p *toy.ListThingsPayload, next toy.Service) (*toy.ThingList, error) {
        log.Printf("ListThings called")
        return next.ListThings(ctx, p)
    })
    return s
}

func patchSingle(next toy.Service) toy.Service {
    s := toyvcr.NewScenario(next)
    s.SetGetThing(func(ctx context.Context, p *toy.GetThingPayload, next toy.Service) (*toy.Thing, error) {
        t, err := next.GetThing(ctx, p)
        if err != nil { return nil, err }
        t.Name = "patched"
        return t, nil
    })
    return s
}

svc := toyvcr.Stack(bg, addLogging, patchSingle)
handler, _ := toyvcr.NewPlaybackHandler(svc)

Streaming methods

Streaming handlers are terminal: they receive (ctx, payload, stream) and do not get a next argument. If a layer has a handler registered for a streaming method, it runs; otherwise the call delegates to the next layer. If no layer handles it, the background returns an explicit error — there are no recorded streams to fall back to.

Stream wrapping (intercepting Send/Recv) is planned for a future release.

CLI scenario registry

The generated CLI accepts repeatable --scenario <name> flags to define an ordered active stack at startup. Scenario objects are applied outer-first: --scenario Happy --scenario Sad means Stack(bg, Happy, Sad).

If --scenario is omitted, DefaultScenarios is used. DefaultScenario remains as a single-entry compatibility fallback.

Registry entries are layer constructors:

cfg := toyvcr.CLIConfig{
    AppName: "toy-vcr",
    ScenarioRegistry: map[string]func(toy.Service) toy.Service{
        "happy": func(next toy.Service) toy.Service {
            s := toyvcr.NewScenario(next)
            s.SetGetThing(func(ctx context.Context, p *toy.GetThingPayload, next toy.Service) (*toy.Thing, error) {
                t, err := next.GetThing(ctx, p)
                if err != nil { return nil, err }
                t.Name = "happy-" + t.Name
                return t, nil
            })
            return s
        },
        "sad": patchSingle,
        "noop": func(next toy.Service) toy.Service { return next },
    },
    DefaultScenarios: []string{"happy", "sad"},
}
os.Exit(toyvcr.RunCLI(os.Args[1:], cfg))

Runtime scenario control API

Playback servers expose an admin API for runtime stack changes:

  • GET /__vcr__/scenarios returns:
    • registered: known scenario names
    • default: startup default stack
    • active: current active stack
  • PUT /__vcr__/scenarios/active replaces the active stack.
  • DELETE /__vcr__/scenarios/active restores startup defaults.

Request body for PUT is an ordered array of objects:

[
  {"name": "Happy"},
  {"name": "Sad"}
]

Semantics:

  • PUT fully replaces the current active stack.
  • PUT [] is valid and means background-only playback.
  • DELETE /__vcr__/scenarios/active restores DefaultScenarios.

VCR Policy (vcr.json)

Each VCR stub directory contains a vcr.json policy file that configures recording behavior:

{
  "upstream": "https://example.com",
  "authorization": {
    "claims": {
      "sub": "deadbeef"
    }
  },
  "endpoints": {
    "GetThing": {
      "variant": {
        "query": false
      }
    }
  }
}

Policy fields:

  • upstream (required): Base URL of the upstream server to proxy to during recording.
  • authorization.claims (optional): Map of required JWT claim names to their required values. When recording, if an Authorization: Bearer <token> header is present, the decoded JWT payload (without signature verification) must contain matching claims. If no Authorization header is present, recording proceeds normally. Claim values must be JSON scalars (string, number, bool, null).
  • endpoints.<name>.variant.query (optional): Controls whether query strings participate in stub variants. Defaults to true if not specified.
  • endpoints.<name>.variant.path (optional): Controls whether route params participate in stub variants. Defaults to true if not specified.

Authorization gate: the authorization.claims policy only affects recording (via RecordingTransport). Playback does not enforce authorization claims; it serves stubs based on endpoint matching and diversifiers only.

About

Record, play back and modify network traffic for Goa services

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages