diff --git a/compute/metadata/go.mod b/compute/metadata/go.mod index 4a3cf5ce461b..003a53465034 100644 --- a/compute/metadata/go.mod +++ b/compute/metadata/go.mod @@ -2,4 +2,7 @@ module cloud.google.com/go/compute/metadata go 1.21 -require golang.org/x/sys v0.28.0 +require ( + github.com/google/go-cmp v0.6.0 + golang.org/x/sys v0.28.0 +) diff --git a/compute/metadata/go.sum b/compute/metadata/go.sum index bc1eec1f07ad..45a728a069d4 100644 --- a/compute/metadata/go.sum +++ b/compute/metadata/go.sum @@ -1,2 +1,4 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/compute/metadata/log_test.go b/compute/metadata/log_test.go new file mode 100644 index 000000000000..7831cb345023 --- /dev/null +++ b/compute/metadata/log_test.go @@ -0,0 +1,137 @@ +// 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 metadata + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "io" + "log/slog" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +var updateGolden = flag.Bool("update-golden", false, "update golden files") + +// To update conformance tests in this package run `go test -update_golden` +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func TestLog_httpRequest(t *testing.T) { + golden := "httpRequest.log" + logger, f := setupLogger(t, golden) + ctx := context.Background() + request, err := http.NewRequest(http.MethodPost, "https://example.com/computeMetadata/v1/instance/service-accounts/default/token", nil) + if err != nil { + t.Fatal(err) + } + request.Header.Add("foo", "bar") + logger.DebugContext(ctx, "msg", "request", httpRequest(request, nil)) + f.Close() + diffTest(t, f.Name(), golden) +} + +func TestLog_httpResponse(t *testing.T) { + golden := "httpResponse.log" + logger, f := setupLogger(t, golden) + ctx := context.Background() + body := []byte(`{"access_token":"token","expires_in":600,"token_type":"Bearer"}`) + response := &http.Response{ + StatusCode: 200, + Header: http.Header{"Foo": []string{"bar"}}, + Body: io.NopCloser(bytes.NewReader(body)), + } + logger.DebugContext(ctx, "msg", "response", httpResponse(response, body)) + f.Close() + diffTest(t, f.Name(), golden) +} + +func setupLogger(t *testing.T, golden string) (*slog.Logger, *os.File) { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), golden) + if err != nil { + t.Fatal(err) + } + logger := slog.New(slog.NewJSONHandler(f, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + return logger, f +} + +// diffTest is a test helper, testing got against contents of a goldenFile. +func diffTest(t *testing.T, tempFile, goldenFile string) { + rawGot, err := os.ReadFile(tempFile) + if err != nil { + t.Fatal(err) + } + t.Helper() + if *updateGolden { + got := removeLogVariance(t, rawGot) + if err := os.WriteFile(filepath.Join("testdata", goldenFile), got, os.ModePerm); err != nil { + t.Fatal(err) + } + return + } + + want, err := os.ReadFile(filepath.Join("testdata", goldenFile)) + if err != nil { + t.Fatal(err) + } + got := removeLogVariance(t, rawGot) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch(-want, +got): %s", diff) + } +} + +// removeLogVariance removes parts of log lines that may differ between runs +// and/or machines. +func removeLogVariance(t *testing.T, in []byte) []byte { + if len(in) == 0 { + return in + } + bs := bytes.Split(in, []byte("\n")) + for i, b := range bs { + if len(b) == 0 { + continue + } + m := map[string]any{} + if err := json.Unmarshal(b, &m); err != nil { + t.Fatal(err) + } + delete(m, "time") + if sl, ok := m["sourceLocation"].(map[string]any); ok { + delete(sl, "file") + // So that if test cases move around in this file they don't cause + // failures + delete(sl, "line") + } + b2, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + t.Logf("%s", b2) + bs[i] = b2 + } + return bytes.Join(bs, []byte("\n")) +} diff --git a/compute/metadata/metadata.go b/compute/metadata/metadata.go index 14293ec106d6..4c18a383a439 100644 --- a/compute/metadata/metadata.go +++ b/compute/metadata/metadata.go @@ -418,9 +418,10 @@ type Client struct { // Options for configuring a [Client]. type Options struct { - // Client is the HTTP used to make requests. Optional. + // Client is the HTTP client used to make requests. Optional. Client *http.Client - // Logger is used to log information about outgoing requests. Optional. + // Logger is used to log information about HTTP request and responses. + // If not provided, nothing will be logged. Optional. Logger *slog.Logger } diff --git a/compute/metadata/testdata/httpRequest.log b/compute/metadata/testdata/httpRequest.log new file mode 100755 index 000000000000..e50f0b1d498e --- /dev/null +++ b/compute/metadata/testdata/httpRequest.log @@ -0,0 +1 @@ +{"level":"DEBUG","msg":"msg","request":{"headers":{"Foo":"bar"},"method":"POST","url":"https://example.com/computeMetadata/v1/instance/service-accounts/default/token"}} diff --git a/compute/metadata/testdata/httpResponse.log b/compute/metadata/testdata/httpResponse.log new file mode 100755 index 000000000000..d26bcba4a795 --- /dev/null +++ b/compute/metadata/testdata/httpResponse.log @@ -0,0 +1 @@ +{"level":"DEBUG","msg":"msg","response":{"headers":{"Foo":"bar"},"payload":{"access_token":"token","expires_in":600,"token_type":"Bearer"},"status":"200"}}