<|start_filename|>lib/notifiers/jsonpath_test.go<|end_filename|>
package notifiers

import (
	"context"
	"fmt"
	"testing"

	"github.com/google/go-cmp/cmp"
	cbpb "google.golang.org/genproto/googleapis/devtools/cloudbuild/v1"
)

func TestNewResolver(t *testing.T) {
	substs := map[string]string{
		"_FOO": "$(thing.other-thing.foo)",
		"_BAR": "$(pizzas[4].cheese)",
	}

	cfg := &Config{
		Spec: &Spec{
			Notification: &Notification{
				Substitutions: substs,
			},
		},
	}

	if _, err := newResolver(cfg); err != nil {
		t.Fatalf("newResolver(%v) failed unexpectedly: %v", cfg, err)
	}
}

func TestNewResolverErrors(t *testing.T) {
	for _, tc := range []struct {
		name   string
		substs map[string]string
	}{{
		name: "no underscore prefix subst name",
		substs: map[string]string{
			"PIZZA": "$(foo.bar[2])",
		},
	}, {
		name: "bad JSONPath",
		substs: map[string]string{
			"_PIZZA": "$(])",
		},
	}, {
		name: "no enclosing $()",
		substs: map[string]string{
			"_PIZZA": "hello.goodbye",
		},
	}} {
		t.Run(tc.name, func(t *testing.T) {
			cfg := &Config{
				Spec: &Spec{
					Notification: &Notification{
						Substitutions: tc.substs,
					},
				},
			}

			if _, err := newResolver(cfg); err == nil {
				t.Errorf("newResolver(%v) unexpectedly succeeded", cfg)
			} else {
				t.Logf("got expected error %v", err)
			}
		})
	}
}

type fakeSecretGetter struct {
	secrets map[string]string
}

func (f *fakeSecretGetter) GetSecret(_ context.Context, secretResource string) (string, error) {
	s, ok := f.secrets[secretResource]
	if !ok {
		return "", fmt.Errorf("secret not stored: %q", secretResource)
	}
	return s, nil
}

func TestResolve(t *testing.T) {
	sg := &fakeSecretGetter{
		secrets: map[string]string{
			"projects/some-project/secrets/some-password/versions/latest": "top-secret",
		},
	}

	secrets := []*Secret{
		{
			LocalName:    "some-password",
			ResourceName: "projects/some-project/secrets/some-password/versions/latest",
		},
	}

	substs := map[string]string{
		"_BUILD_STATUS":        "$(build.status)",
		"_BRANCH_NAME":         "$(build.substitutions.BRANCH_NAME)",
		"_COMMIT_AUTHOR_EMAIL": "$(build.substitutions._COMMIT_AUTHOR_EMAIL)",
		"_SOME_PASSWORD":       "$(<PASSWORD>)",
		"_ALL_STEPS":           "$(build.steps[*].name)",
		"_MY_TRIGGER_ID":       "$(build.build_trigger_id)",
	}

	cfg := &Config{
		Spec: &Spec{
			Notification: &Notification{
				Substitutions: substs,
			},
			Secrets: secrets,
		},
	}

	r, err := newResolver(cfg)
	if err != nil {
		t.Fatal(err)
	}

	build := &cbpb.Build{
		Status: cbpb.Build_SUCCESS,
		Steps: []*cbpb.BuildStep{
			{Name: "foo"},
			{Name: "bar"},
			{Name: "baz"},
		},
		Substitutions: map[string]string{
			"BRANCH_NAME":          "my-branch",
			"_COMMIT_AUTHOR_EMAIL": "<EMAIL>",
		},
	}

	gotResolved, err := r.Resolve(context.Background(), sg, build)
	if err != nil {
		t.Fatal(err)
	}

	wantResolved := map[string]string{
		"$_BRANCH_NAME":         "my-branch",
		"$_BUILD_STATUS":        "SUCCESS",
		"$_COMMIT_AUTHOR_EMAIL": "<EMAIL>",
		"$_SOME_PASSWORD":       "<PASSWORD>",
		"$_ALL_STEPS":           "foo bar baz",
		"$_MY_TRIGGER_ID":       "",
	}

	if diff := cmp.Diff(wantResolved, gotResolved); diff != "" {
		t.Errorf("unxpected diff from resolving JSONPath:\n%s", diff)
	}
}

func TestResolveErrors(t *testing.T) {
	for _, tc := range []struct {
		name    string
		substs  map[string]string
		secrets []*Secret
		build   *cbpb.Build
	}{{
		name: "path for list with bad index",
		substs: map[string]string{
			"_FOO": "$(build.tags[404])",
		},
		build: &cbpb.Build{Id: "id", Tags: []string{"some-tag"}},
	}, {
		name: "path for unknown subst",
		substs: map[string]string{
			"_FOO": "$(build.substitutions['DNE'])",
		},
		build: &cbpb.Build{Substitutions: map[string]string{"_HELLO": "world"}},
	}, {
		name: "path for unknown field",
		substs: map[string]string{
			"_FOO": "$(build.banana)",
		},
		build: &cbpb.Build{Id: "id"},
	}, {
		name: "path for unknown secret",
		substs: map[string]string{
			"_FOO": "$(secrets['not-found'])",
		},
	}, {
		name: "path for unfetchable secret",
		substs: map[string]string{
			"_FOO": "$(secrets['bad-secret'])",
		},
		secrets: []*Secret{
			{
				LocalName:    "bad-secret",
				ResourceName: "projects/foo/secrets/bad-secret/version/latest",
			},
		},
	}} {
		t.Run(tc.name, func(t *testing.T) {
			cfg := &Config{
				Spec: &Spec{
					Notification: &Notification{
						Substitutions: tc.substs,
					},
					Secrets: tc.secrets,
				},
			}
			r, err := newResolver(cfg)
			if err != nil {
				t.Fatalf("newResolver(...) failed unexpectedly: %v", err)
			}

			// Any secrets we try to look up will result in errors.
			sg := new(fakeSecretGetter)

			if _, err := r.Resolve(context.Background(), sg, tc.build); err == nil {
				t.Error("Resolve unexpectedly succeeded")
			} else {
				t.Logf("got expected error: %v", err)
			}
		})
	}
}


<|start_filename|>slack/main_test.go<|end_filename|>
package main

import (
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/slack-go/slack"
	cbpb "google.golang.org/genproto/googleapis/devtools/cloudbuild/v1"
)

func TestWriteMessage(t *testing.T) {
	n := new(slackNotifier)
	b := &cbpb.Build{
		ProjectId: "my-project-id",
		Id:        "some-build-id",
		Status:    cbpb.Build_SUCCESS,
		LogUrl:    "https://some.example.com/log/url?foo=bar",
	}

	got, err := n.writeMessage(b)
	if err != nil {
		t.Fatalf("writeMessage failed: %v", err)
	}

	want := &slack.WebhookMessage{
		Attachments: []slack.Attachment{{
			Text:  "Cloud Build (my-project-id, some-build-id): SUCCESS",
			Color: "good",
			Actions: []slack.AttachmentAction{{
				Text: "View Logs",
				Type: "button",
				URL:  "https://some.example.com/log/url?foo=bar&utm_campaign=google-cloud-build-notifiers&utm_medium=chat&utm_source=google-cloud-build",
			}},
		}},
	}

	if diff := cmp.Diff(got, want); diff != "" {
		t.Errorf("writeMessage got unexpected diff: %s", diff)
	}
}


<|start_filename|>http/main_test.go<|end_filename|>
// Copyright 2020 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 main

import (
	"context"
	"testing"

	"github.com/GoogleCloudPlatform/cloud-build-notifiers/lib/notifiers"
)

func TestSetUp(t *testing.T) {
	const url = "https://some.example.com/notify"

	for _, tc := range []struct {
		name    string
		cfg     *notifiers.Config
		wantErr bool
	}{{
		name: "valid config",
		cfg: &notifiers.Config{
			Spec: &notifiers.Spec{
				Notification: &notifiers.Notification{
					Filter: `build.status == Build.Status.SUCCESS`,
					Delivery: map[string]interface{}{
						"url": url,
					},
				},
			},
		},
	}, {
		name: "missing filter",
		cfg: &notifiers.Config{
			Spec: &notifiers.Spec{
				Notification: &notifiers.Notification{
					Delivery: map[string]interface{}{
						"url": url,
					},
				},
			},
		},
		wantErr: true,
	}, {
		name: "bad filter",
		cfg: &notifiers.Config{
			Spec: &notifiers.Spec{
				Notification: &notifiers.Notification{
					Filter: "blah-#B A D#-",
					Delivery: map[string]interface{}{
						"url": url,
					},
				},
			},
		},
		wantErr: true,
	}, {
		name: "missing delivery url",
		cfg: &notifiers.Config{
			Spec: &notifiers.Spec{
				Notification: &notifiers.Notification{
					Filter: `build.status == Build.Status.SUCCESS`,
					Delivery: map[string]interface{}{
						"foo": "bar",
					},
				},
			},
		},
		wantErr: true,
	}, {
		name: "non-string `url`",
		cfg: &notifiers.Config{
			Spec: &notifiers.Spec{
				Notification: &notifiers.Notification{
					Filter: `build.status == Build.Status.SUCCESS`,
					Delivery: map[string]interface{}{
						"url": 404,
					},
				},
			},
		},
		wantErr: true,
	}} {
		t.Run(tc.name, func(t *testing.T) {
			n := new(httpNotifier)
			err := n.SetUp(context.Background(), tc.cfg, nil, nil)
			if err != nil {
				if tc.wantErr {
					t.Logf("got expected error: %v", err)
					return
				}
				t.Fatalf("SetUp(%v) got unexpected error: %v", tc.cfg, err)
			}

			if tc.wantErr {
				t.Error("unexpected success")
			}

			if n.url != tc.cfg.Spec.Notification.Delivery["url"].(string) {
				t.Errorf("mismatch in post-setup URL: got %q; want %q", n.url, tc.cfg.Spec.Notification.Delivery["url"])
			}
		})
	}
}


<|start_filename|>bigquery/bqi.go<|end_filename|>
// Copyright 2020 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 main

import "context"

type bqFactory interface {
	Make(ctx context.Context) (bq, error)
}

type bq interface {
	EnsureDataset(ctx context.Context, datasetName string) error
	EnsureTable(ctx context.Context, tableName string) error
	WriteRow(ctx context.Context, r *bqRow) error
}


<|start_filename|>lib/notifiers/jsonpath.go<|end_filename|>
package notifiers

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"regexp"
	"strings"
	"sync"

	cbpb "google.golang.org/genproto/googleapis/devtools/cloudbuild/v1"
	"k8s.io/client-go/third_party/forked/golang/template"
	"k8s.io/client-go/util/jsonpath"
)

var (
	subNamePattern = regexp.MustCompile("^_[A-Z][0-9A-Z_]*$")
)

// BindingResolver is an object that given a Build and a way to get secrets, returns all bound substitutions from the
// notifier configuration.
type BindingResolver interface {
	Resolve(context.Context, SecretGetter, *cbpb.Build) (map[string]string, error)
}

type inputAndJSONPath struct {
	j *jsonpath.JSONPath // The JSONPath that parsed that path.
	p string             // The user-provided path.
}

type jpResolver struct {
	mtx sync.RWMutex
	jps map[string]*inputAndJSONPath // Map of _SOME_SUBST_NAME => its inputAndJSONPath.
	cfg *Config
}

func newResolver(cfg *Config) (BindingResolver, error) {
	jps := map[string]*inputAndJSONPath{}
	for name, path := range cfg.Spec.Notification.Substitutions {
		if !subNamePattern.MatchString(name) {
			return nil, fmt.Errorf("expected name %q to match pattern %v", name, subNamePattern)
		}
		p, err := makeJSONPath(path)
		if err != nil {
			return nil, fmt.Errorf("failed to derive substitution path from %q: %v", path, err)
		}
		j := jsonpath.New(name).AllowMissingKeys(false)
		if err := j.Parse(p); err != nil {
			return nil, fmt.Errorf("failed to parse JSONPath expression from %q: %v", path, err)
		}

		jps[name] = &inputAndJSONPath{
			j: j,
			p: path, // Use the user-provided path so error messages are easier to understand.
		}
	}
	return &jpResolver{
		jps: jps,
		cfg: cfg,
	}, nil
}

func (j *jpResolver) Resolve(ctx context.Context, sg SecretGetter, build *cbpb.Build) (map[string]string, error) {
	j.mtx.RLock()
	defer j.mtx.RUnlock()

	// Fetch all of the secrets that might appear in the given paths.
	// TODO(ljr): We can try to be clever and only use the ones that are mentioned in the user-provided paths.
	sm := map[string]string{}
	for _, s := range j.cfg.Spec.Secrets {
		sv, err := sg.GetSecret(ctx, s.ResourceName)
		if err != nil {
			return nil, fmt.Errorf("failed to get secret value for resource %q: %v", s.ResourceName, err)
		}
		sm[s.LocalName] = sv
	}

	// Use a "JSON" payload here since a struct would have export-field issues
	// based on the lowercase names.
	pld := map[string]interface{}{
		"build":   build,
		"secrets": sm,
	}

	ret := map[string]string{}
	for name, jp := range j.jps {
		fullResults, err := jp.j.FindResults(pld)
		if err != nil {
			return nil, fmt.Errorf("failed to parse %q with path %q from payload: %v", name, jp.p, err)
		}

		if len(fullResults) == 0 {
			return nil, fmt.Errorf("failed to get JSONPath query results for %q with path %q", name, jp.p)
		}

		buf := new(bytes.Buffer)
		for _, r := range fullResults {
			if err := printResults(buf, r); err != nil {
				return nil, err
			}
		}
		ret["$"+name] = buf.String()
	}
	return ret, nil
}

func makeJSONPath(path string) (string, error) {
	if !strings.HasPrefix(path, "$(") || !strings.HasSuffix(path, ")") {
		return "", fmt.Errorf("expected %q to start with `$(` and end with `)` for a valid JSONPath expression", path)
	}
	trimmed := strings.TrimSuffix(strings.TrimPrefix(path, "$("), ")")
	return fmt.Sprintf("{ .%s }", trimmed), nil
}

/**

NOTE; THE FOLLOWING HAS BEEN SHAMELESSLY COPIED FROM
https://github.com/tektoncd/triggers/blob/master/pkg/template/jsonpath.go

**/

func printResults(wr io.Writer, results []reflect.Value) error {
	for i, r := range results {
		text, err := textValue(r)
		if err != nil {
			return err
		}
		if i != len(results)-1 {
			text = append(text, ' ')
		}
		if _, err := wr.Write(text); err != nil {
			return err
		}
	}
	return nil
}

func textValue(v reflect.Value) ([]byte, error) {
	t := reflect.TypeOf(v.Interface())
	// special case for null values in JSON; evalToText() returns <nil> here
	if t == nil {
		return []byte("null"), nil
	}

	switch t.Kind() {
	// evalToText() returns <map> ....; return JSON string instead.
	case reflect.Map, reflect.Slice:
		return json.Marshal(v.Interface())
	default:
		return evalToText(v)
	}
}

func evalToText(v reflect.Value) ([]byte, error) {
	iface, ok := template.PrintableValue(v)
	if !ok {
		// only happens if v is a Chan or a Func
		return nil, fmt.Errorf("can't print type %s", v.Type())
	}
	var buffer bytes.Buffer
	fmt.Fprint(&buffer, iface)
	return buffer.Bytes(), nil
}
