Repository: tektoncd/pipeline (language: go)

Issue:
Add tests for pkg/resolution/resource
We have no tests for this module; see discussion on https://github.com/tektoncd/pipeline/pull/6424


Discussion / hints:
/assign
I will probably do it next weekend.

Fix patch:
diff --git a/pkg/resolution/common/interface.go b/pkg/resolution/common/interface.go
new file mode 100644
index 00000000000..b1c07165b35
--- /dev/null
+++ b/pkg/resolution/common/interface.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2023 The Tekton Authors
+
+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 common
+
+import (
+	"context"
+
+	pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// ResolverName is the type used for a resolver's name and is mostly
+// used to ensure the function signatures that accept it are clear on the
+// purpose for the given string.
+type ResolverName string
+
+// Requester is the interface implemented by a type that knows how to
+// submit requests for remote resources.
+type Requester interface {
+	// Submit accepts the name of a resolver to submit a request to
+	// along with the request itself.
+	Submit(context.Context, ResolverName, Request) (ResolvedResource, error)
+}
+
+// Request is implemented by any type that represents a single request
+// for a remote resource. Implementing this interface gives the underlying
+// type an opportunity to control properties such as whether the name of
+// a request has particular properties, whether the request should be made
+// to a specific namespace, and precisely which parameters should be included.
+type Request interface {
+	Name() string
+	Namespace() string
+	Params() pipelinev1beta1.Params
+}
+
+// OwnedRequest is implemented by any type implementing Request that also needs
+// to express a Kubernetes OwnerRef relationship as part of the request being
+// made.
+type OwnedRequest interface {
+	OwnerRef() metav1.OwnerReference
+}
+
+// ResolvedResource is implemented by any type that offers a read-only
+// view of the data and metadata of a resolved remote resource.
+type ResolvedResource interface {
+	Data() ([]byte, error)
+	Annotations() map[string]string
+	RefSource() *pipelinev1beta1.RefSource
+}
diff --git a/pkg/resolution/resource/crd_resource.go b/pkg/resolution/resource/crd_resource.go
index 328be3508ea..f50eebe65c4 100644
--- a/pkg/resolution/resource/crd_resource.go
+++ b/pkg/resolution/resource/crd_resource.go
@@ -122,7 +122,13 @@ func appendOwnerReference(rr *v1beta1.ResolutionRequest, req Request) {
 }
 
 func ownerRefsAreEqual(a, b metav1.OwnerReference) bool {
-	return a.APIVersion == b.APIVersion && a.Kind == b.Kind && a.Name == b.Name && a.UID == b.UID && a.Controller == b.Controller
+	// pointers values cannot be directly compared.
+	if (a.Controller == nil && b.Controller != nil) ||
+		(a.Controller != nil && b.Controller == nil) ||
+		(*a.Controller != *b.Controller) {
+		return false
+	}
+	return a.APIVersion == b.APIVersion && a.Kind == b.Kind && a.Name == b.Name && a.UID == b.UID
 }
 
 // readOnlyResolutionRequest is an opaque wrapper around ResolutionRequest
diff --git a/pkg/resolution/resource/name.go b/pkg/resolution/resource/name.go
index 115b584c723..a48d0a4d5e3 100644
--- a/pkg/resolution/resource/name.go
+++ b/pkg/resolution/resource/name.go
@@ -42,6 +42,9 @@ func GenerateDeterministicName(prefix, base string, params v1beta1.Params) (stri
 	}
 
 	sortedParams := make(v1beta1.Params, len(params))
+	for i := range params {
+		sortedParams[i] = *params[i].DeepCopy()
+	}
 	sort.SliceStable(sortedParams, func(i, j int) bool {
 		return sortedParams[i].Name < sortedParams[j].Name
 	})
diff --git a/pkg/resolution/resource/resource.go b/pkg/resolution/resource/resource.go
index b18b9d7d4d7..9952cdbf65b 100644
--- a/pkg/resolution/resource/resource.go
+++ b/pkg/resolution/resource/resource.go
@@ -17,47 +17,32 @@ limitations under the License.
 package resource
 
 import (
-	"context"
-
-	pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"github.com/tektoncd/pipeline/pkg/resolution/common"
 )
 
+// This is an alias for avoiding cycle import
+
 // ResolverName is the type used for a resolver's name and is mostly
 // used to ensure the function signatures that accept it are clear on the
 // purpose for the given string.
-type ResolverName string
+type ResolverName = common.ResolverName
 
 // Requester is the interface implemented by a type that knows how to
 // submit requests for remote resources.
-type Requester interface {
-	// Submit accepts the name of a resolver to submit a request to
-	// along with the request itself.
-	Submit(context.Context, ResolverName, Request) (ResolvedResource, error)
-}
+type Requester = common.Requester
 
 // Request is implemented by any type that represents a single request
 // for a remote resource. Implementing this interface gives the underlying
 // type an opportunity to control properties such as whether the name of
 // a request has particular properties, whether the request should be made
 // to a specific namespace, and precisely which parameters should be included.
-type Request interface {
-	Name() string
-	Namespace() string
-	Params() pipelinev1beta1.Params
-}
+type Request = common.Request
 
 // OwnedRequest is implemented by any type implementing Request that also needs
 // to express a Kubernetes OwnerRef relationship as part of the request being
 // made.
-type OwnedRequest interface {
-	OwnerRef() metav1.OwnerReference
-}
+type OwnedRequest = common.OwnedRequest
 
 // ResolvedResource is implemented by any type that offers a read-only
 // view of the data and metadata of a resolved remote resource.
-type ResolvedResource interface {
-	Data() ([]byte, error)
-	Annotations() map[string]string
-	RefSource() *pipelinev1beta1.RefSource
-}
+type ResolvedResource = common.ResolvedResource


Test patch:
diff --git a/pkg/resolution/resource/crd_resource_test.go b/pkg/resolution/resource/crd_resource_test.go
new file mode 100644
index 00000000000..da5a06fac38
--- /dev/null
+++ b/pkg/resolution/resource/crd_resource_test.go
@@ -0,0 +1,321 @@
+/*
+Copyright 2023 The Tekton Authors
+
+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 resource_test
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1"
+	ttesting "github.com/tektoncd/pipeline/pkg/reconciler/testing"
+	resolutioncommon "github.com/tektoncd/pipeline/pkg/resolution/common"
+	"github.com/tektoncd/pipeline/pkg/resolution/resource"
+	"github.com/tektoncd/pipeline/test"
+	"github.com/tektoncd/pipeline/test/diff"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/logging"
+	_ "knative.dev/pkg/system/testing" // Setup system.Namespace()
+	"sigs.k8s.io/yaml"
+)
+
+// getCRDRequester returns an instance of the CRDRequester that has been seeded with
+// d, where d represents the state of the system (existing resources) needed for the test.
+func getCRDRequester(t *testing.T, d test.Data) (test.Assets, func()) {
+	t.Helper()
+	return initializeCRDRequesterAssets(t, d)
+}
+
+func initializeCRDRequesterAssets(t *testing.T, d test.Data) (test.Assets, func()) {
+	t.Helper()
+	ctx, _ := ttesting.SetupFakeContext(t)
+	ctx, cancel := context.WithCancel(ctx)
+	c, informers := test.SeedTestData(t, ctx, d)
+
+	return test.Assets{
+		Logger:    logging.FromContext(ctx),
+		Clients:   c,
+		Informers: informers,
+		Ctx:       ctx,
+	}, cancel
+}
+
+func TestCRDRequesterSubmit(t *testing.T) {
+	ownerRef := mustParseOwnerReference(t, `
+apiVersion: tekton.dev/v1beta1
+blockOwnerDeletion: true
+controller: true
+kind: TaskRun
+name: git-clone
+uid: 727019c3-4066-4d8b-919e-90660dfd8b55
+`)
+	request := mustParseRawRequest(t, `
+name: git-ec247f5592afcaefa8485e34d2bd80c6
+namespace: namespace
+params:
+  - name: url
+    value: https://github.com/tektoncd/catalog
+  - name: revision
+    value: main
+  - name: pathInRepo
+    value: task/git-clone/0.6/git-clone.yaml
+`)
+	baseRR := mustParseResolutionRequest(t, `
+kind: "ResolutionRequest"
+apiVersion: "resolution.tekton.dev/v1beta1"
+metadata:
+  name: "git-ec247f5592afcaefa8485e34d2bd80c6"
+  namespace: "namespace"
+  labels:
+    resolution.tekton.dev/type: "git"
+  ownerReferences:
+  - apiVersion: tekton.dev/v1beta1
+    blockOwnerDeletion: true
+    controller: true
+    kind: TaskRun
+    name: git-clone
+    uid: 727019c3-4066-4d8b-919e-90660dfd8b55
+spec:
+  params:
+    - name: "url"
+      value: "https://github.com/tektoncd/catalog"
+    - name: "revision"
+      value: "main"
+    - name: "pathInRepo"
+      value: "task/git-clone/0.6/git-clone.yaml"
+`)
+	createdRR := baseRR.DeepCopy()
+	//
+	unknownRR := baseRR.DeepCopy()
+	unknownRR.Status = *mustParseResolutionRequestStatus(t, `
+conditions:
+  - lastTransitionTime: "2023-03-26T10:31:29Z"
+    status: "Unknown"
+    type: Succeeded
+`)
+	//
+	failedRR := baseRR.DeepCopy()
+	failedRR.Status = *mustParseResolutionRequestStatus(t, `
+conditions:
+  - lastTransitionTime: "2023-03-26T10:31:29Z"
+    status: "Failed"
+    type: Succeeded
+    message: "error message"
+`)
+	//
+	successRR := baseRR.DeepCopy()
+	successRR.Status = *mustParseResolutionRequestStatus(t, `
+annotations:
+  resolution.tekton.dev/content-type: application/x-yaml
+  resolution.tekton.dev/path: task/git-clone/0.6/git-clone.yaml
+  resolution.tekton.dev/revision: main
+  resolution.tekton.dev/url: https://github.com/tektoncd/catalog
+conditions:
+  - lastTransitionTime: "2023-03-26T10:31:29Z"
+    status: "True"
+    type: Succeeded
+    data: e30=
+`)
+	//
+	successWithoutAnnotationsRR := baseRR.DeepCopy()
+	successWithoutAnnotationsRR.Status = *mustParseResolutionRequestStatus(t, `
+conditions:
+  - lastTransitionTime: "2023-03-26T10:31:29Z"
+    status: "True"
+    type: Succeeded
+    data: e30=
+`)
+
+	testCases := []struct {
+		name                      string
+		inputRequest              *test.RawRequest
+		inputResolutionRequest    *v1beta1.ResolutionRequest
+		expectedResolutionRequest *v1beta1.ResolutionRequest
+		expectedResolvedResource  *v1beta1.ResolutionRequest
+		expectedErr               error
+	}{
+		{
+			name:                      "resolution request does not exist and needs to be created",
+			inputRequest:              request,
+			inputResolutionRequest:    nil,
+			expectedResolutionRequest: createdRR.DeepCopy(),
+			expectedResolvedResource:  nil,
+			expectedErr:               resolutioncommon.ErrRequestInProgress,
+		},
+		{
+			name:                      "resolution request exist and status is unknown",
+			inputRequest:              request,
+			inputResolutionRequest:    unknownRR.DeepCopy(),
+			expectedResolutionRequest: nil,
+			expectedResolvedResource:  nil,
+			expectedErr:               resolutioncommon.ErrRequestInProgress,
+		},
+		{
+			name:                      "resolution request exist and status is succeeded",
+			inputRequest:              request,
+			inputResolutionRequest:    successRR.DeepCopy(),
+			expectedResolutionRequest: nil,
+			expectedResolvedResource:  successRR.DeepCopy(),
+			expectedErr:               nil,
+		},
+		{
+			name:                      "resolution request exist and status is succeeded but annotations is nil",
+			inputRequest:              request,
+			inputResolutionRequest:    successWithoutAnnotationsRR.DeepCopy(),
+			expectedResolutionRequest: nil,
+			expectedResolvedResource:  successWithoutAnnotationsRR.DeepCopy(),
+			expectedErr:               nil,
+		},
+		{
+			name:                      "resolution request exist and status is failed",
+			inputRequest:              request,
+			inputResolutionRequest:    failedRR.DeepCopy(),
+			expectedResolutionRequest: nil,
+			expectedResolvedResource:  nil,
+			expectedErr:               resolutioncommon.NewError(resolutioncommon.ReasonResolutionFailed, errors.New("error message")),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			d := test.Data{}
+			if tc.inputResolutionRequest != nil {
+				d.ResolutionRequests = []*v1beta1.ResolutionRequest{tc.inputResolutionRequest}
+			}
+
+			testAssets, cancel := getCRDRequester(t, d)
+			defer cancel()
+			ctx := testAssets.Ctx
+			clients := testAssets.Clients
+
+			resolver := resolutioncommon.ResolverName("git")
+			crdRequester := resource.NewCRDRequester(clients.ResolutionRequests, testAssets.Informers.ResolutionRequest.Lister())
+			requestWithOwner := &ownerRequest{
+				Request:  tc.inputRequest.Request(),
+				ownerRef: *ownerRef,
+			}
+			resolvedResource, err := crdRequester.Submit(ctx, resolver, requestWithOwner)
+
+			// check the error
+			if err != nil || tc.expectedErr != nil {
+				if err == nil || tc.expectedErr == nil {
+					t.Errorf("expected error %v, but got %v", tc.expectedErr, err)
+				} else if err.Error() != tc.expectedErr.Error() {
+					t.Errorf("expected error %v, but got %v", tc.expectedErr, err)
+				}
+			}
+
+			// check the resolved resource
+			switch {
+			case tc.expectedResolvedResource == nil:
+				// skipping check of resolved resources.
+			case tc.expectedResolvedResource != nil:
+				if resolvedResource == nil {
+					t.Errorf("expected resolved resource equal %v, but got %v", tc.expectedResolvedResource, resolvedResource)
+					break
+				}
+				rr := tc.expectedResolvedResource
+				data, err := base64.StdEncoding.Strict().DecodeString(rr.Status.Data)
+				if err != nil {
+					t.Errorf("unexpected error decoding expected resource data: %v", err)
+				}
+				expectedResolvedResource := test.NewResolvedResource(data, rr.Status.Annotations, rr.Status.RefSource, nil)
+				assertResolvedResourceEqual(t, expectedResolvedResource, resolvedResource)
+			}
+
+			// check the resolution request
+			if tc.expectedResolutionRequest != nil {
+				resolutionrequest, err := clients.ResolutionRequests.ResolutionV1beta1().
+					ResolutionRequests(tc.inputRequest.Namespace).Get(ctx, tc.inputRequest.Name, metav1.GetOptions{})
+				if err != nil {
+					t.Errorf("unexpected error getting resource requests: %v", err)
+				}
+				if d := cmp.Diff(tc.expectedResolutionRequest, resolutionrequest); d != "" {
+					t.Errorf("expected resolution request to match %s", diff.PrintWantGot(d))
+				}
+			}
+		})
+	}
+}
+
+type ownerRequest struct {
+	resolutioncommon.Request
+	ownerRef metav1.OwnerReference
+}
+
+func (r *ownerRequest) OwnerRef() metav1.OwnerReference {
+	return r.ownerRef
+}
+
+func mustParseRawRequest(t *testing.T, yamlStr string) *test.RawRequest {
+	t.Helper()
+	output := &test.RawRequest{}
+	if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil {
+		t.Errorf("parsing raw request %s: %v", yamlStr, err)
+	}
+	return output
+}
+
+func mustParseOwnerReference(t *testing.T, yamlStr string) *metav1.OwnerReference {
+	t.Helper()
+	output := &metav1.OwnerReference{}
+	if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil {
+		t.Errorf("parsing owner reference %s: %v", yamlStr, err)
+	}
+	return output
+}
+
+func mustParseResolutionRequest(t *testing.T, yamlStr string) *v1beta1.ResolutionRequest {
+	t.Helper()
+	output := &v1beta1.ResolutionRequest{}
+	if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil {
+		t.Errorf("parsing resolution request %s: %v", yamlStr, err)
+	}
+	return output
+}
+
+func mustParseResolutionRequestStatus(t *testing.T, yamlStr string) *v1beta1.ResolutionRequestStatus {
+	t.Helper()
+	output := &v1beta1.ResolutionRequestStatus{}
+	if err := yaml.Unmarshal([]byte(yamlStr), output); err != nil {
+		t.Errorf("parsing resolution request status %s: %v", yamlStr, err)
+	}
+	return output
+}
+
+func assertResolvedResourceEqual(t *testing.T, expected, actual resolutioncommon.ResolvedResource) {
+	t.Helper()
+	expectedBytes, err := expected.Data()
+	if err != nil {
+		t.Errorf("unexpected error getting expected resource data: %v", err)
+	}
+	actualBytes, err := actual.Data()
+	if err != nil {
+		t.Errorf("unexpected error getting acutal resource data: %v", err)
+	}
+	if d := cmp.Diff(expectedBytes, actualBytes); d != "" {
+		t.Errorf("expected resolved resource Data to match %s", diff.PrintWantGot(d))
+	}
+	if d := cmp.Diff(expected.Annotations(), actual.Annotations()); d != "" {
+		t.Errorf("expected resolved resource Annotations to match %s", diff.PrintWantGot(d))
+	}
+	if d := cmp.Diff(expected.RefSource(), actual.RefSource()); d != "" {
+		t.Errorf("expected resolved resource Source to match %s", diff.PrintWantGot(d))
+	}
+}
diff --git a/pkg/resolution/resource/name_test.go b/pkg/resolution/resource/name_test.go
new file mode 100644
index 00000000000..07bc26d7cc2
--- /dev/null
+++ b/pkg/resolution/resource/name_test.go
@@ -0,0 +1,115 @@
+/*
+Copyright 2023 The Tekton Authors
+
+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 resource_test
+
+import (
+	"testing"
+
+	"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+	"github.com/tektoncd/pipeline/pkg/resolution/resource"
+)
+
+func TestGenerateDeterministicName(t *testing.T) {
+	type args struct {
+		prefix string
+		base   string
+		params []v1beta1.Param
+	}
+	golden := args{
+		prefix: "prefix",
+		base:   "base",
+		params: []v1beta1.Param{
+			{
+				Name: "string-param",
+				Value: v1beta1.ParamValue{
+					Type:      v1beta1.ParamTypeString,
+					StringVal: "value1",
+				},
+			},
+			{
+				Name: "array-param",
+				Value: v1beta1.ParamValue{
+					Type:     v1beta1.ParamTypeArray,
+					ArrayVal: []string{"value1", "value2"},
+				},
+			},
+			{
+				Name: "object-param",
+				Value: v1beta1.ParamValue{
+					Type:      v1beta1.ParamTypeObject,
+					ObjectVal: map[string]string{"key": "value"},
+				},
+			},
+		},
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		{
+			name: "only contains prefix",
+			args: args{
+				prefix: golden.prefix,
+			},
+			want: "prefix-6c62272e07bb014262b821756295c58d",
+		},
+		{
+			name: "only contains base",
+			args: args{
+				base: golden.base,
+			},
+			want: "-6989337ae0757277b806e97e86444ef0",
+		},
+		{
+			name: "only contains params",
+			args: args{
+				params: golden.params,
+			},
+			want: "-52921b17d3c2930a34419c618d6af0e9",
+		},
+		{
+			name: "params with different order should generate same hash",
+			args: args{
+				params: []v1beta1.Param{
+					golden.params[2],
+					golden.params[1],
+					golden.params[0],
+				},
+			},
+			want: "-52921b17d3c2930a34419c618d6af0e9",
+		},
+		{
+			name: "contain all fields",
+			args: golden,
+			want: "prefix-ba2f256f318de7f4154da577c283cb9e",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := resource.GenerateDeterministicName(tt.args.prefix, tt.args.base, tt.args.params)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GenerateDeterministicName() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("GenerateDeterministicName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/resolution/resource/request_test.go b/pkg/resolution/resource/request_test.go
new file mode 100644
index 00000000000..d7605a783a4
--- /dev/null
+++ b/pkg/resolution/resource/request_test.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2023 The Tekton Authors
+
+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 resource_test
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
+	"github.com/tektoncd/pipeline/pkg/resolution/resource"
+	"github.com/tektoncd/pipeline/test/diff"
+)
+
+func TestNewRequest(t *testing.T) {
+	type args struct {
+		name      string
+		namespace string
+		params    v1beta1.Params
+	}
+	type want = args
+	golden := args{
+		name:      "test-name",
+		namespace: "test-namespace",
+		params: v1beta1.Params{
+			{Name: "param1", Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "value1"}},
+			{Name: "param2", Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "value2"}},
+		},
+	}
+	tests := []struct {
+		name string
+		args args
+		want want
+	}{
+		{
+			name: "empty",
+			args: args{},
+			want: want{},
+		},
+		{
+			name: "all",
+			args: golden,
+			want: golden,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			request := resource.NewRequest(tt.args.name, tt.args.namespace, tt.args.params)
+			if request == nil {
+				t.Errorf("NewRequest() return nil")
+			}
+			if request.Name() != tt.want.name {
+				t.Errorf("NewRequest().Name() = %v, want %v", request.Name(), tt.want.name)
+			}
+			if request.Namespace() != tt.want.namespace {
+				t.Errorf("NewRequest().Namespace() = %v, want %v", request.Namespace(), tt.want.namespace)
+			}
+			if d := cmp.Diff(request.Params(), tt.want.params); d != "" {
+				t.Errorf("expected params to match %s", diff.PrintWantGot(d))
+			}
+		})
+	}
+}
diff --git a/test/resolution.go b/test/resolution.go
index d136d0e19a3..7d46d6ac100 100644
--- a/test/resolution.go
+++ b/test/resolution.go
@@ -24,7 +24,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
-	resolution "github.com/tektoncd/pipeline/pkg/resolution/resource"
+	resolution "github.com/tektoncd/pipeline/pkg/resolution/common"
 	"github.com/tektoncd/pipeline/test/diff"
 )
 
@@ -122,3 +122,57 @@ func (r *ResolvedResource) Annotations() map[string]string {
 func (r *ResolvedResource) RefSource() *pipelinev1beta1.RefSource {
 	return r.ResolvedRefSource
 }
+
+// RawRequest stores the raw request data
+type RawRequest struct {
+	// the request name
+	Name string
+	// the request namespace
+	Namespace string
+	// the params for the request
+	Params []pipelinev1beta1.Param
+}
+
+// Request returns a Request interface based on the RawRequest.
+func (r *RawRequest) Request() resolution.Request {
+	if r == nil {
+		r = &RawRequest{}
+	}
+	return &Request{
+		RawRequest: *r,
+	}
+}
+
+// Request implements resolution.Request and makes it easier to mock input for submit
+// Using inline structs is to avoid conflicts between field names and method names.
+type Request struct {
+	RawRequest
+}
+
+var _ resolution.Request = &Request{}
+
+// NewRequest creates a mock request that is populated with the given name namespace and params
+func NewRequest(name, namespace string, params []pipelinev1beta1.Param) *Request {
+	return &Request{
+		RawRequest: RawRequest{
+			Name:      name,
+			Namespace: namespace,
+			Params:    params,
+		},
+	}
+}
+
+// Name implements resolution.Request and returns the mock name given to it on initialization.
+func (r *Request) Name() string {
+	return r.RawRequest.Name
+}
+
+// Namespace implements resolution.Request and returns the mock namespace given to it on initialization.
+func (r *Request) Namespace() string {
+	return r.RawRequest.Namespace
+}
+
+// Params implements resolution.Request and returns the mock params given to it on initialization.
+func (r *Request) Params() pipelinev1beta1.Params {
+	return r.RawRequest.Params
+}