Skip to content

Commit

Permalink
Record HTTP requests in tests
Browse files Browse the repository at this point in the history
We can then verify the exact request/respone content.
  • Loading branch information
justinsb committed Feb 5, 2024
1 parent 8b7d5b9 commit caffd60
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 63 deletions.
38 changes: 24 additions & 14 deletions config/tests/samples/create/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Harness struct {
*testing.T
Ctx context.Context

Events *test.MemoryEventSink
Project testgcp.GCPProject

client client.Client
Expand Down Expand Up @@ -275,47 +276,56 @@ func NewHarness(t *testing.T, ctx context.Context) *Harness {
h.Project = testgcp.GetDefaultProject(t)
}

// Log DCL requests
eventSink := test.NewMemoryEventSink()
ctx = test.AddSinkToContext(ctx, eventSink)
h.Ctx = ctx

h.Events = eventSink

eventSinks := test.EventSinksFromContext(ctx)

// Set up event sink for logging to a file, if ARTIFACTS env var is set
if artifacts := os.Getenv("ARTIFACTS"); artifacts != "" {
outputDir := filepath.Join(artifacts, "http-logs")
eventSinks = append(eventSinks, test.NewDirectoryEventSink(outputDir))
} else {
log.Info("env var ARTIFACTS is not set; will not record http log")
}

// Intercept (and log) DCL requests
if len(eventSinks) != 0 {
if kccConfig.HTTPClient == nil {
httpClient, err := google.DefaultClient(ctx, gcp.ClientScopes...)
if err != nil {
t.Fatalf("error creating the http client to be used by DCL: %v", err)
}
kccConfig.HTTPClient = httpClient
}
t := test.NewHTTPRecorder(kccConfig.HTTPClient.Transport, outputDir)
t := test.NewHTTPRecorder(kccConfig.HTTPClient.Transport, eventSinks...)
kccConfig.HTTPClient = &http.Client{Transport: t}
}

// Log TF requests
// Intercept (and log) TF requests
transport_tpg.DefaultHTTPClientTransformer = func(ctx context.Context, inner *http.Client) *http.Client {
ret := inner
if t := ctx.Value(httpRoundTripperKey); t != nil {
ret = &http.Client{Transport: t.(http.RoundTripper)}
}
if artifacts := os.Getenv("ARTIFACTS"); artifacts == "" {
log.Info("env var ARTIFACTS is not set; will not record http log")
} else {
outputDir := filepath.Join(artifacts, "http-logs")
t := test.NewHTTPRecorder(ret.Transport, outputDir)
if len(eventSinks) != 0 {
t := test.NewHTTPRecorder(ret.Transport, eventSinks...)
ret = &http.Client{Transport: t}
}
return ret
}

// Log TF oauth requests
// Intercept (and log) TF oauth requests
transport_tpg.OAuth2HTTPClientTransformer = func(ctx context.Context, inner *http.Client) *http.Client {
ret := inner
if t := ctx.Value(httpRoundTripperKey); t != nil {
ret = &http.Client{Transport: t.(http.RoundTripper)}
}
if artifacts := os.Getenv("ARTIFACTS"); artifacts == "" {
log.Info("env var ARTIFACTS is not set; will not record http log")
} else {
outputDir := filepath.Join(artifacts, "http-logs")
t := test.NewHTTPRecorder(ret.Transport, outputDir)
if len(eventSinks) != 0 {
t := test.NewHTTPRecorder(ret.Transport, eventSinks...)
ret = &http.Client{Transport: t}
}
return ret
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/gcpclient/client_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func init() {
return inner
}
outputDir := filepath.Join(artifacts, "http-logs")
t := test.NewHTTPRecorder(inner.Transport, outputDir)
t := test.NewHTTPRecorder(inner.Transport, test.NewDirectoryEventSink(outputDir))
return &http.Client{Transport: t}
}
transport_tpg.OAuth2HTTPClientTransformer = func(ctx context.Context, inner *http.Client) *http.Client {
Expand All @@ -93,7 +93,7 @@ func init() {
return inner
}
outputDir := filepath.Join(artifacts, "http-logs")
t := test.NewHTTPRecorder(inner.Transport, outputDir)
t := test.NewHTTPRecorder(inner.Transport, test.NewDirectoryEventSink(outputDir))
return &http.Client{Transport: t}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func init() {
log.Info("env var ARTIFACTS is not set; will not record http log")
} else {
outputDir := filepath.Join(artifacts, "http-logs")
t := test.NewHTTPRecorder(ret.Transport, outputDir)
t := test.NewHTTPRecorder(ret.Transport, test.NewDirectoryEventSink(outputDir))
ret = &http.Client{Transport: t}
}
return ret
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/mocktests/secretmanager_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestSecretManagerSecretVersion(t *testing.T) {
} else {
outputDir := filepath.Join(artifacts, "http-logs")

roundTripper = test.NewHTTPRecorder(mockCloud, outputDir)
roundTripper = test.NewHTTPRecorder(mockCloud, test.NewDirectoryEventSink(outputDir))
}

gcpHTTPClient := &http.Client{Transport: roundTripper}
Expand Down
13 changes: 10 additions & 3 deletions pkg/dcl/clientconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,28 @@ func New(ctx context.Context, opt Options) (*dcl.Config, error) {
// Deprecated: Prefer using a harness.
func NewForIntegrationTest() *dcl.Config {
ctx := context.TODO()
eventSinks := test.EventSinksFromContext(ctx)

if artifacts := os.Getenv("ARTIFACTS"); artifacts != "" {
outputDir := filepath.Join(artifacts, "http-logs")

eventSinks = append(eventSinks, test.NewDirectoryEventSink(outputDir))
}

opt := Options{
UserAgent: "kcc/dev",
}

// Log DCL requests
if artifacts := os.Getenv("ARTIFACTS"); artifacts != "" {
outputDir := filepath.Join(artifacts, "http-logs")
if len(eventSinks) != 0 {
if opt.HTTPClient == nil {
httpClient, err := google.DefaultClient(ctx, gcp.ClientScopes...)
if err != nil {
klog.Fatalf("error creating the http client to be used by DCL: %v", err)
}
opt.HTTPClient = httpClient
}
t := test.NewHTTPRecorder(opt.HTTPClient.Transport, outputDir)
t := test.NewHTTPRecorder(opt.HTTPClient.Transport, eventSinks...)
opt.HTTPClient = &http.Client{Transport: t}
}

Expand Down
173 changes: 173 additions & 0 deletions pkg/test/eventsink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package test

import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"

"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
)

// An EventSink listens for various events we are able to capture during tests,
// currently just http requests/responses.
type EventSink interface {
AddHTTPEvent(ctx context.Context, entry *LogEntry)
}

type httpEventSinkType int

var httpEventSinkKey httpEventSinkType

// EventSinksFromContext gets the EventSink listeners attached to the passed context.
func EventSinksFromContext(ctx context.Context) []EventSink {
v := ctx.Value(httpEventSinkKey)
if v == nil {
return nil
}
return v.([]EventSink)
}

// AddSinkToContext attaches the sinks to the returned context.
func AddSinkToContext(ctx context.Context, sinks ...EventSink) context.Context {
var eventSinks []EventSink
v := ctx.Value(httpEventSinkKey)
if v != nil {
eventSinks = v.([]EventSink)
}
eventSinks = append(eventSinks, sinks...)
return context.WithValue(ctx, httpEventSinkKey, eventSinks)
}

func NewMemoryEventSink() *MemoryEventSink {
return &MemoryEventSink{}
}

// MemoryEventSink is an EventSink that stores events in memory
type MemoryEventSink struct {
mutex sync.Mutex
HTTPEvents []*LogEntry `json:"httpEvents,omitempty"`
}

func (s *MemoryEventSink) AddHTTPEvent(ctx context.Context, entry *LogEntry) {
s.mutex.Lock()
defer s.mutex.Unlock()

s.HTTPEvents = append(s.HTTPEvents, entry)
}

func (s *MemoryEventSink) FormatHTTP() string {
s.mutex.Lock()
defer s.mutex.Unlock()

var eventStrings []string
for _, entry := range s.HTTPEvents {
s := entry.FormatHTTP()
eventStrings = append(eventStrings, s)
}
return strings.Join(eventStrings, "\n---\n\n")
}

func (s *MemoryEventSink) PrettifyJSON(mutators ...JSONMutator) {
s.mutex.Lock()
defer s.mutex.Unlock()

for _, entry := range s.HTTPEvents {
entry.PrettifyJSON(mutators...)
}
}

func (s *MemoryEventSink) RemoveHTTPResponseHeader(key string) {
s.mutex.Lock()
defer s.mutex.Unlock()

for _, entry := range s.HTTPEvents {
entry.Response.RemoveHeader(key)
}
}

func (s *MemoryEventSink) RemoveRequests(pred func(e *LogEntry) bool) {
s.mutex.Lock()
defer s.mutex.Unlock()

var keep []*LogEntry
for _, entry := range s.HTTPEvents {
if !pred(entry) {
keep = append(keep, entry)
}
}
s.HTTPEvents = keep
}

type DirectoryEventSink struct {
outputDir string

// mutex to avoid concurrent writes to the same file
mutex sync.Mutex
}

func NewDirectoryEventSink(outputDir string) *DirectoryEventSink {
return &DirectoryEventSink{outputDir: outputDir}
}

func (r *DirectoryEventSink) AddHTTPEvent(ctx context.Context, entry *LogEntry) {
// Write to a log file
t := TestFromContext(ctx)
testName := "unknown"
if t != nil {
testName = t.Name()
}
dirName := sanitizePath(testName)
p := filepath.Join(r.outputDir, dirName, "requests.log")

if err := r.writeToFile(p, entry); err != nil {
klog.Fatalf("error writing http event: %v", err)
}
}

func (r *DirectoryEventSink) writeToFile(p string, entry *LogEntry) error {
b, err := yaml.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}

// Just in case we are writing to the same file concurrently
r.mutex.Lock()
defer r.mutex.Unlock()

if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
return fmt.Errorf("failed to create directory %q: %w", filepath.Dir(p), err)
}
f, err := os.OpenFile(p, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
return fmt.Errorf("failed to open file %q: %w", p, err)
}
defer f.Close()

if _, err := f.Write(b); err != nil {
return fmt.Errorf("failed to write to file %q: %w", p, err)
}
delimeter := "\n\n---\n\n"
if _, err := f.Write([]byte(delimeter)); err != nil {
return fmt.Errorf("failed to write to file %q: %w", p, err)
}

return nil
}
Loading

0 comments on commit caffd60

Please sign in to comment.