Repository: goadesign/goa (go)

Issue:
bug: Interceptor ReadPayload attributes can't find attributes defined using Extend()
Interceptors seem neat! I tried one out and ran into this issue where ReadPayload attributes can't find attributes defined using Extend(). If I replace the `Extend` with the attribute definition ex: `Attribute("tenantID", String, "Tenant ID for the request")`, the generation works as expected. 

Minimal example below:

```
var _ = Service("example", func() {
	ServerInterceptor(ResourceID)

	Method("get", func() {
		Payload(func() {
			Extend(CommonAttributes)
			Attribute("id", String)
			Required("tenantID", "id")
		})

		Result(Int)

		HTTP(func() {
			GET("{tenantID}/resource/{id}")
			Response(StatusOK)
		})
	})
})

var ResourceID = Interceptor("TenantID", func() {
	ReadPayload(func() {
		Attribute("tenantID", String, "Tenant ID for request")
	})
})

var CommonAttributes = Type("Common", func() {
	Attribute("tenantID", String, "Tenant ID for the request")
})
```

`gen` output:
`service "example" method "get": interceptor "TenantID" cannot read payload attribute "tenantID": attribute does not exist`

Expected:
The server interceptor can find all of the attributes, including those that are defined using Extend. 

Pull request description:
Properly consider bases when validating interceptors
Fixes #3664

Patch:
diff --git a/expr/interceptor.go b/expr/interceptor.go
index 31a24335..ab73930d 100644
--- a/expr/interceptor.go
+++ b/expr/interceptor.go
@@ -46,15 +46,23 @@ func (i *InterceptorExpr) validate(m *MethodExpr) *eval.ValidationErrors {
 	verr := new(eval.ValidationErrors)
 
 	if i.ReadPayload != nil || i.WritePayload != nil {
-		payloadObj := AsObject(m.Payload.Type)
-		if payloadObj == nil {
+		if !IsObject(m.Payload.Type) {
 			verr.Add(m, "interceptor %q cannot be applied because the method payload is not an object", i.Name)
-		}
-		if i.ReadPayload != nil {
-			i.validateAttributeAccess(m, "read payload", verr, payloadObj, i.ReadPayload)
-		}
-		if i.WritePayload != nil {
-			i.validateAttributeAccess(m, "write payload", verr, payloadObj, i.WritePayload)
+		} else {
+			payload := DupAtt(m.Payload)
+			if m.Payload.Bases != nil {
+				for _, base := range m.Payload.Bases {
+					if ut, ok := base.(UserType); ok {
+						payload.Merge(ut.Attribute())
+					}
+				}
+			}
+			if i.ReadPayload != nil {
+				i.validateAttributeAccess(m, "read payload", verr, AsObject(payload.Type), i.ReadPayload)
+			}
+			if i.WritePayload != nil {
+				i.validateAttributeAccess(m, "write payload", verr, AsObject(payload.Type), i.WritePayload)
+			}
 		}
 	}
 
@@ -62,47 +70,73 @@ func (i *InterceptorExpr) validate(m *MethodExpr) *eval.ValidationErrors {
 		if m.IsResultStreaming() {
 			verr.Add(m, "interceptor %q cannot be applied because the method result is streaming", i.Name)
 		}
-		resultObj := AsObject(m.Result.Type)
-		if resultObj == nil {
+		if !IsObject(m.Result.Type) {
 			verr.Add(m, "interceptor %q cannot be applied because the method result is not an object", i.Name)
-		}
-		if i.ReadResult != nil {
-			i.validateAttributeAccess(m, "read result", verr, resultObj, i.ReadResult)
-		}
-		if i.WriteResult != nil {
-			i.validateAttributeAccess(m, "write result", verr, resultObj, i.WriteResult)
+		} else {
+			result := DupAtt(m.Result)
+			if m.Result.Bases != nil {
+				for _, base := range m.Result.Bases {
+					if ut, ok := base.(UserType); ok {
+						result.Merge(ut.Attribute())
+					}
+				}
+			}
+			if i.ReadResult != nil {
+				i.validateAttributeAccess(m, "read result", verr, AsObject(result.Type), i.ReadResult)
+			}
+			if i.WriteResult != nil {
+				i.validateAttributeAccess(m, "write result", verr, AsObject(result.Type), i.WriteResult)
+			}
 		}
 	}
 
 	if i.ReadStreamingPayload != nil || i.WriteStreamingPayload != nil {
-		if !m.IsPayloadStreaming() {
+		if !m.IsPayloadStreaming() || m.StreamingPayload == nil {
 			verr.Add(m, "interceptor %q cannot be applied because the method payload is not streaming", i.Name)
-		}
-		payloadObj := AsObject(m.StreamingPayload.Type)
-		if payloadObj == nil {
-			verr.Add(m, "interceptor %q cannot be applied because the method payload is not an object", i.Name)
-		}
-		if i.ReadStreamingPayload != nil {
-			i.validateAttributeAccess(m, "read streaming payload", verr, payloadObj, i.ReadStreamingPayload)
-		}
-		if i.WriteStreamingPayload != nil {
-			i.validateAttributeAccess(m, "write streaming payload", verr, payloadObj, i.WriteStreamingPayload)
+		} else {
+			if !IsObject(m.StreamingPayload.Type) {
+				verr.Add(m, "interceptor %q cannot be applied because the method payload is not an object", i.Name)
+			} else {
+				payload := DupAtt(m.StreamingPayload)
+				if m.StreamingPayload.Bases != nil {
+					for _, base := range m.StreamingPayload.Bases {
+						if ut, ok := base.(UserType); ok {
+							payload.Merge(ut.Attribute())
+						}
+					}
+				}
+				if i.ReadStreamingPayload != nil {
+					i.validateAttributeAccess(m, "read streaming payload", verr, AsObject(payload.Type), i.ReadStreamingPayload)
+				}
+				if i.WriteStreamingPayload != nil {
+					i.validateAttributeAccess(m, "write streaming payload", verr, AsObject(payload.Type), i.WriteStreamingPayload)
+				}
+			}
 		}
 	}
 
 	if i.ReadStreamingResult != nil || i.WriteStreamingResult != nil {
 		if !m.IsResultStreaming() {
 			verr.Add(m, "interceptor %q cannot be applied because the method result is not streaming", i.Name)
-		}
-		resultObj := AsObject(m.Result.Type)
-		if resultObj == nil {
-			verr.Add(m, "interceptor %q cannot be applied because the method result is not an object", i.Name)
-		}
-		if i.ReadStreamingResult != nil {
-			i.validateAttributeAccess(m, "read streaming result", verr, resultObj, i.ReadStreamingResult)
-		}
-		if i.WriteStreamingResult != nil {
-			i.validateAttributeAccess(m, "write streaming result", verr, resultObj, i.WriteStreamingResult)
+		} else {
+			if !IsObject(m.Result.Type) {
+				verr.Add(m, "interceptor %q cannot be applied because the method result is not an object", i.Name)
+			} else {
+				result := DupAtt(m.Result)
+				if m.Result.Bases != nil {
+					for _, base := range m.Result.Bases {
+						if ut, ok := base.(UserType); ok {
+							result.Merge(ut.Attribute())
+						}
+					}
+				}
+				if i.ReadStreamingResult != nil {
+					i.validateAttributeAccess(m, "read streaming result", verr, AsObject(result.Type), i.ReadStreamingResult)
+				}
+				if i.WriteStreamingResult != nil {
+					i.validateAttributeAccess(m, "write streaming result", verr, AsObject(result.Type), i.WriteStreamingResult)
+				}
+			}
 		}
 	}
 


Test patch:
diff --git a/expr/interceptor_test.go b/expr/interceptor_test.go
new file mode 100644
index 00000000..fa8fd1ab
--- /dev/null
+++ b/expr/interceptor_test.go
@@ -0,0 +1,196 @@
+package expr
+
+import (
+	"testing"
+)
+
+func TestInterceptorExpr_Validate(t *testing.T) {
+	cases := map[string]struct {
+		intercept  *InterceptorExpr
+		method     *MethodExpr
+		wantErrors []string
+	}{
+		"valid-payload": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadPayload(t, namedAttr(t, "foo"))),
+			method:    makeMethod(t, withPayload(t, namedAttr(t, "foo"))),
+		},
+		"valid-write-payload": {
+			intercept: makeInterceptor(t, "test-interceptor", withWritePayload(t, namedAttr(t, "foo"))),
+			method:    makeMethod(t, withPayload(t, namedAttr(t, "foo"))),
+		},
+		"payload-with-base": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadPayload(t, namedAttr(t, "bar"))),
+			method: makeMethod(t,
+				withPayload(t, namedAttr(t, "foo")),
+				withPayloadBases(t, &UserTypeExpr{
+					AttributeExpr: &AttributeExpr{
+						Type: &Object{namedAttr(t, "bar")},
+					},
+				}),
+			),
+		},
+		"result-with-base": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadResult(t, namedAttr(t, "bar"))),
+			method: makeMethod(t,
+				withResult(t, namedAttr(t, "foo")),
+				withResultBases(t, &UserTypeExpr{
+					AttributeExpr: &AttributeExpr{
+						Type: &Object{namedAttr(t, "bar")},
+					},
+				}),
+			),
+		},
+		"invalid-payload-not-object": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadPayload(t, namedAttr(t, "foo"))),
+			method: makeMethod(t, func(m *MethodExpr) {
+				m.Payload = &AttributeExpr{
+					Type: String,
+				}
+			}),
+			wantErrors: []string{
+				`interceptor "test-interceptor" cannot be applied because the method payload is not an object`,
+			},
+		},
+		"invalid-streaming-payload-not-streaming": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadStreamingPayload(t, namedAttr(t, "foo"))),
+			method: makeMethod(t, func(m *MethodExpr) {
+				m.Payload = &AttributeExpr{Type: &Object{}}
+			}),
+			wantErrors: []string{
+				`interceptor "test-interceptor" cannot be applied because the method payload is not streaming`,
+			},
+		},
+		"invalid-streaming-result-not-streaming": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadStreamingResult(t, namedAttr(t, "foo"))),
+			method: makeMethod(t, func(m *MethodExpr) {
+				m.Result = &AttributeExpr{Type: &Object{}}
+			}),
+			wantErrors: []string{
+				`interceptor "test-interceptor" cannot be applied because the method result is not streaming`,
+			},
+		},
+		"invalid-attribute-access": {
+			intercept: makeInterceptor(t, "test-interceptor", withReadPayload(t, namedAttr(t, "bar"))),
+			method:    makeMethod(t, withPayload(t, namedAttr(t, "foo"))),
+			wantErrors: []string{
+				`interceptor "test-interceptor" cannot read payload attribute "bar": attribute does not exist`,
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			verr := tc.intercept.validate(tc.method)
+			if len(tc.wantErrors) != len(verr.Errors) {
+				t.Errorf("got %d errors, expected %d", len(verr.Errors), len(tc.wantErrors))
+			}
+			for i, err := range verr.Errors {
+				if i >= len(tc.wantErrors) {
+					break
+				}
+				if got := err.Error(); got != tc.wantErrors[i] {
+					t.Errorf("got error %q, expected %q", got, tc.wantErrors[i])
+				}
+			}
+		})
+	}
+}
+
+// Test helpers (at the end of the file)
+func withWritePayload(t *testing.T, attrs *NamedAttributeExpr) func(*InterceptorExpr) {
+	t.Helper()
+	return func(i *InterceptorExpr) {
+		i.WritePayload = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func withResultBases(t *testing.T, bases ...DataType) func(*MethodExpr) {
+	t.Helper()
+	return func(m *MethodExpr) {
+		if m.Result == nil {
+			m.Result = &AttributeExpr{Type: &Object{}}
+		}
+		m.Result.Bases = bases
+	}
+}
+
+func makeInterceptor(t *testing.T, name string, opts ...func(*InterceptorExpr)) *InterceptorExpr {
+	t.Helper()
+	i := &InterceptorExpr{Name: name}
+	for _, opt := range opts {
+		opt(i)
+	}
+	return i
+}
+
+// Helper functions need to be updated to handle empty attributes properly
+func withReadPayload(t *testing.T, attrs *NamedAttributeExpr) func(*InterceptorExpr) {
+	t.Helper()
+	return func(i *InterceptorExpr) {
+		i.ReadPayload = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func withReadResult(t *testing.T, attrs *NamedAttributeExpr) func(*InterceptorExpr) {
+	t.Helper()
+	return func(i *InterceptorExpr) {
+		i.ReadResult = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func withReadStreamingPayload(t *testing.T, attrs *NamedAttributeExpr) func(*InterceptorExpr) {
+	t.Helper()
+	return func(i *InterceptorExpr) {
+		i.ReadStreamingPayload = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func withReadStreamingResult(t *testing.T, attrs *NamedAttributeExpr) func(*InterceptorExpr) {
+	t.Helper()
+	return func(i *InterceptorExpr) {
+		i.ReadStreamingResult = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func makeMethod(t *testing.T, opts ...func(*MethodExpr)) *MethodExpr {
+	t.Helper()
+	m := &MethodExpr{}
+	for _, opt := range opts {
+		opt(m)
+	}
+	return m
+}
+
+func withPayload(t *testing.T, attrs *NamedAttributeExpr) func(*MethodExpr) {
+	t.Helper()
+	return func(m *MethodExpr) {
+		m.Payload = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func withPayloadBases(t *testing.T, bases ...DataType) func(*MethodExpr) {
+	t.Helper()
+	return func(m *MethodExpr) {
+		if m.Payload == nil {
+			m.Payload = &AttributeExpr{Type: &Object{}}
+		}
+		m.Payload.Bases = bases
+	}
+}
+
+func withResult(t *testing.T, attrs *NamedAttributeExpr) func(*MethodExpr) {
+	t.Helper()
+	return func(m *MethodExpr) {
+		m.Result = &AttributeExpr{Type: &Object{attrs}}
+	}
+}
+
+func namedAttr(t *testing.T, name string) *NamedAttributeExpr {
+	t.Helper()
+	return &NamedAttributeExpr{
+		Name: name,
+		Attribute: &AttributeExpr{
+			Type: String,
+		},
+	}
+}