Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cli/azd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d
google.golang.org/grpc v1.83.2
google.golang.org/protobuf v1.36.11
gopkg.in/dnaeon/go-vcr.v3 v3.2.0
gopkg.in/dnaeon/go-vcr.v4 v4.0.7
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -154,6 +154,7 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.38.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions cli/azd/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
Expand Down Expand Up @@ -426,8 +428,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM=
gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag=
gopkg.in/dnaeon/go-vcr.v4 v4.0.7 h1:Mq/RF+mq3QwtEunJSsoTbYPt3elSAmdJhAxrEaqr88I=
gopkg.in/dnaeon/go-vcr.v4 v4.0.7/go.mod h1:cRwV/njsN/D8qNJu4NAXWswz6b4OUh3rMIu4SObbLBg=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/cmdrecord/cmdrecorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"strings"
"sync"

"gopkg.in/dnaeon/go-vcr.v3/recorder"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

const ProxyConfigName = "proxy.config"
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/cmdrecord/cmdrecorder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

"github.com/braydonk/yaml"
"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v3/recorder"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

func TestMain(m *testing.M) {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/cmdrecord/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

"github.com/azure/azure-dev/cli/azd/test/cmdrecord"
"github.com/braydonk/yaml"
"gopkg.in/dnaeon/go-vcr.v3/recorder"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

type ErrExitCode struct {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/func_publish_poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"net/url"
"strings"

"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

// funcPublishPoller handles polling for Azure Functions flex consumption publish operations.
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/func_publish_poller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"testing"

"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

func Test_funcPublishPoller_BeforeSave(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/poll_discard.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"net/url"
"strings"

"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

// httpPollDiscarder discards awaiting-done polling interactions from the cassette.
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/poll_discard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"testing"

"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

func Test_httpPollDiscarder_BeforeSave(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"strings"
"syscall"

"gopkg.in/dnaeon/go-vcr.v3/recorder"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

// gzipReader wraps a response body so it can lazily
Expand Down
103 changes: 49 additions & 54 deletions cli/azd/test/recording/recording.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import (

"github.com/azure/azure-dev/cli/azd/test/cmdrecord"
"github.com/braydonk/yaml"
"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v3/recorder"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/recorder"
)

type recordOptions struct {
Expand Down Expand Up @@ -157,42 +157,12 @@ func Start(t *testing.T, opts ...Options) *Session {
Level: level,
}))

recorderOptions := &recorder.Options{
CassetteName: name,
Mode: opt.mode,
SkipRequestLatency: true,
recorderOptions := []recorder.Option{
recorder.WithMode(opt.mode),
recorder.WithSkipRequestLatency(true),
}

// This also automatically loads the recording.
vcr, err := recorder.NewWithOptions(recorderOptions)
if errors.Is(err, os.ErrNotExist) {
t.Fatalf("failed to load recordings: %v: %s",
err,
"to record this test, re-run the test with AZURE_RECORD_MODE='record'")
} else if err != nil {
t.Fatalf("failed to load recordings: %v", err)
}

var vcr *recorder.Recorder
session := &Session{}
if opt.mode == recorder.ModeReplayOnly {
session.Playback = true
} else if opt.mode == recorder.ModeRecordOnce && !vcr.IsNewCassette() {
session.Playback = true
}

if session.Playback {
variables, err := loadVariables(name + ".yaml")
if err != nil {
t.Fatalf("failed to load variables: %v", err)
}
session.Variables = variables
if session.Variables == nil { // prefer empty map over nil
session.Variables = map[string]string{}
}
} else {
session.Variables = map[string]string{}
session.Variables[TimeKey] = fmt.Sprintf("%d", time.Now().Unix())
}

transport := http.DefaultTransport.(*http.Transport).Clone()

Expand All @@ -201,9 +171,9 @@ func Start(t *testing.T, opts ...Options) *Session {
transport: transport,
})

vcr.SetRealTransport(resilientTransport)
recorderOptions = append(recorderOptions, recorder.WithRealTransport(resilientTransport))

vcr.SetMatcher(func(r *http.Request, i cassette.Request) bool {
recorderOptions = append(recorderOptions, recorder.WithMatcher(func(r *http.Request, i cassette.Request) bool {
// Ignore query parameter 's=...' in containerappOperationResults
if strings.Contains(r.URL.Path, "/providers/Microsoft.App/") &&
strings.Contains(r.URL.Path, "/containerappOperationResults") {
Expand Down Expand Up @@ -237,19 +207,20 @@ func Start(t *testing.T, opts ...Options) *Session {
}
}

return cassette.DefaultMatcher(r, i)
})
// Existing cassettes use v3's method/URL matching and sanitize transient request metadata.
return r.Method == i.Method && r.URL.String() == i.URL
}))

// Fast-forward polling operations
discarder := httpPollDiscarder{}
vcr.AddHook(discarder.BeforeSave, recorder.BeforeSaveHook)
recorderOptions = append(recorderOptions, recorder.WithHook(discarder.BeforeSave, recorder.BeforeSaveHook))

// Trim GET subscriptions-level deployment responses
vcr.AddHook(func(i *cassette.Interaction) error {
recorderOptions = append(recorderOptions, recorder.WithHook(func(i *cassette.Interaction) error {
return TrimSubscriptionsDeployment(i, session.Variables)
}, recorder.BeforeSaveHook)
}, recorder.BeforeSaveHook))

vcr.AddHook(func(i *cassette.Interaction) error {
recorderOptions = append(recorderOptions, recorder.WithHook(func(i *cassette.Interaction) error {
if opt.hostMapping == nil {
return nil
}
Expand All @@ -264,10 +235,10 @@ func Start(t *testing.T, opts ...Options) *Session {
}

return nil
}, recorder.AfterCaptureHook)
}, recorder.AfterCaptureHook))

// Sanitize
vcr.AddHook(func(i *cassette.Interaction) error {
recorderOptions = append(recorderOptions, recorder.WithHook(func(i *cassette.Interaction) error {
i.Request.Headers.Set("Authorization", "SANITIZED")

err := sanitizeContainerAppTokenExchange(i)
Expand Down Expand Up @@ -301,26 +272,26 @@ func Start(t *testing.T, opts ...Options) *Session {
}

return nil
}, recorder.BeforeSaveHook)
}, recorder.BeforeSaveHook))

vcr.AddHook(func(i *cassette.Interaction) error {
recorderOptions = append(recorderOptions, recorder.WithHook(func(i *cassette.Interaction) error {
if i.DiscardOnSave {
log.Debug("recorderProxy: discarded response", "url", i.Request.URL, "status", i.Response.Code)
}
return nil
}, recorder.BeforeSaveHook)
}, recorder.BeforeSaveHook))

vcr.AddHook(func(i *cassette.Interaction) error {
recorderOptions = append(recorderOptions, recorder.WithHook(func(i *cassette.Interaction) error {
if vcr.IsRecording() {
log.Debug("recorderProxy: recording response", "url", i.Request.URL, "status", i.Response.Code)
} else {
log.Debug("recorderProxy: replaying response", "url", i.Request.URL, "status", i.Response.Code)
}
return nil
}, recorder.BeforeResponseReplayHook)
}, recorder.BeforeResponseReplayHook))

// Add passthrough for services that return personal data and need not be recorded
vcr.AddPassthrough(func(req *http.Request) bool {
recorderOptions = append(recorderOptions, recorder.WithPassthrough(func(req *http.Request) bool {
return strings.Contains(req.URL.Host, "login.microsoftonline.com") ||
strings.Contains(req.URL.Host, "graph.microsoft.com") ||
strings.Contains(req.URL.Host, "applicationinsights.azure.com") ||
Expand All @@ -337,7 +308,31 @@ func Start(t *testing.T, opts ...Options) *Session {
// In playback mode, RemoteCredential calls this server for tokens.
(strings.Contains(req.URL.Host, "127.0.0.1") &&
req.URL.Path == "/token")
})
}))

// Configure all hooks before loading the cassette; callbacks run after initialization.
vcr, err := recorder.New(name, recorderOptions...)
if errors.Is(err, os.ErrNotExist) {
t.Fatalf("failed to load recordings: %v: %s",
err,
"to record this test, re-run the test with AZURE_RECORD_MODE='record'")
} else if err != nil {
t.Fatalf("failed to load recordings: %v", err)
}
session.Playback = opt.mode == recorder.ModeReplayOnly ||
(opt.mode == recorder.ModeRecordOnce && !vcr.IsNewCassette())
if session.Playback {
variables, err := loadVariables(name + ".yaml")
if err != nil {
t.Fatalf("failed to load variables: %v", err)
}
session.Variables = variables
if session.Variables == nil {
session.Variables = map[string]string{}
}
} else {
session.Variables = map[string]string{TimeKey: fmt.Sprintf("%d", time.Now().Unix())}
}

proxy := &connectHandler{
Log: log,
Expand Down Expand Up @@ -402,7 +397,7 @@ func Start(t *testing.T, opts ...Options) *Session {
}

if shouldSave {
err = saveVariables(recorderOptions.CassetteName+".yaml", session.Variables)
err = saveVariables(name+".yaml", session.Variables)
if err != nil {
t.Fatalf("failed to save variables: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/sanitize.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry"
"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

func sanitizeContainerAppTokenExchange(i *cassette.Interaction) error {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/sanitize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3"
"github.com/stretchr/testify/require"
"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

func TestSanitizeContainerAppUpdate_IgnoresOtherPatchRequests(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/test/recording/trim_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/azure/azure-dev/cli/azd/pkg/azure"
"gopkg.in/dnaeon/go-vcr.v3/cassette"
"gopkg.in/dnaeon/go-vcr.v4/pkg/cassette"
)

// Matches a request with path ending in subscriptions/<guid>/providers/Microsoft.Resources/deployments,
Expand Down
Loading