Repository: goadesign/goa
Pull request #3243 (2023-02-08 21:07:33)

Fix use of `struct:pkg:path` with result types.
Fixes https://github.com/goadesign/goa/issues/3240


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0155ea70..2531b8e1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,10 +11,10 @@ jobs:
       uses: runforesight/foresight-workflow-kit-action@v1
       if: success() || failure()
 
-    - name: Set up Go 1.19
+    - name: Set up Go 1.20
       uses: actions/setup-go@v3.5.0
       with:
-        go-version: 1.19
+        go-version: '1.20'
       id: go
 
     - name: Check out code into the Go module directory
@@ -55,10 +55,10 @@ jobs:
       uses: runforesight/foresight-workflow-kit-action@v1
       if: success() || failure()
 
-    - name: Set up Go 1.19
+    - name: Set up Go 1.20
       uses: actions/setup-go@v3.5.0
       with:
-        go-version: 1.19
+        go-version: '1.20'
       id: go
 
     - name: Check out code into the Go module directory
@@ -90,4 +90,4 @@ jobs:
         test_framework: GOLANG
         test_path: test-report.json
         coverage_format: GOLANG
-        coverage_path: .\coverage
\ No newline at end of file
+        coverage_path: .\coverage
diff --git a/Makefile b/Makefile
index bdff24f9..5a94607b 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@
 #
 MAJOR=3
 MINOR=11
-BUILD=0
+BUILD=1
 
 GOOS=$(shell go env GOOS)
 GO_FILES=$(shell find . -type f -name '*.go')
diff --git a/README.md b/README.md
index 5cb7fbea..4b44e413 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 ## Overview
 
-[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.11.0/dsl?tab=doc)
+[![GoDev](https://img.shields.io/badge/go-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/goa.design/goa/v3@v3.11.1/dsl?tab=doc)
 [![Foresight](https://api-public.service.runforesight.com/api/v1/badge/test?repoId=67defc2e-b3f8-4043-bac7-a4e215518ed3)](http://goa.app.runforesight.com/)
 [![Go Report Card](https://goreportcard.com/badge/goadesign/goa)](https://goreportcard.com/report/goadesign/goa)
 [![Slack](https://img.shields.io/badge/Slack-gray.svg?longCache=true&logo=slack&colorB=red)](https://gophers.slack.com/messages/goa)
@@ -86,7 +86,7 @@ invoking the client code.
 go install goa.design/goa/v3/cmd/goa@v3
 ```
 
-Current Release: `v3.11.0`
+Current Release: `v3.11.1`
 
 ## Teaser
 
@@ -352,7 +352,7 @@ In particular the page
 explains how to leverage the generated code to implement an HTTP or gRPC
 service.
 
-The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.11.0/dsl?tab=doc)
+The [![DSL GoDoc](https://img.shields.io/badge/godoc-DSL-blue)](https://pkg.go.dev/goa.design/goa/v3@v3.11.1/dsl?tab=doc)
 contains a fully documented reference of all the DSL functions.
 
 ### Instrumentation and System Example
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index 3e38b166..e723135c 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -1251,6 +1251,7 @@ func buildProjectedType(projected, att *expr.AttributeExpr, viewspkg string, sco
 		}
 		validations = buildValidations(projected, viewScope)
 	}
+	removeMeta(projected)
 	return &ProjectedTypeData{
 		UserTypeData: &UserTypeData{
 			Name:        varname,
@@ -1796,6 +1797,16 @@ func walkViewAttrs(obj *expr.Object, view *expr.ViewExpr, walker func(name strin
 	}
 }
 
+// removeMeta removes the meta attributes from the given attribute. This is
+// needed to make sure that any field name overridding is removed when
+// generating protobuf types (as protogen itself won't honor these overrides).
+func removeMeta(att *expr.AttributeExpr) {
+	_ = codegen.Walk(att, func(a *expr.AttributeExpr) error {
+		delete(a.Meta, "struct:pkg:path")
+		return nil
+	})
+}
+
 const (
 	initTypeCodeT = `{{ if or .ToResult .ToViewed }}
 	{{- if eq (len .Views) 1 }}
diff --git a/go.mod b/go.mod
index 31a91840..cfe97943 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module goa.design/goa/v3
 
-go 1.19
+go 1.20
 
 require (
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
@@ -8,14 +8,13 @@ require (
 	github.com/getkin/kin-openapi v0.112.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.5.0
-	github.com/hashicorp/go-multierror v1.1.1
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
 	github.com/pkg/errors v0.9.1
 	github.com/sergi/go-diff v1.3.1
 	github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
-	golang.org/x/text v0.6.0
-	golang.org/x/tools v0.5.0
-	google.golang.org/grpc v1.52.3
+	golang.org/x/text v0.7.0
+	golang.org/x/tools v0.6.0
+	google.golang.org/grpc v1.53.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -25,16 +24,15 @@ require (
 	github.com/go-openapi/swag v0.19.13 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
-	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/invopop/yaml v0.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/mailru/easyjson v0.7.6 // indirect
 	github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/smartystreets/goconvey v1.7.2 // indirect
-	golang.org/x/mod v0.7.0 // indirect
-	golang.org/x/net v0.5.0 // indirect
-	golang.org/x/sys v0.4.0 // indirect
-	google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
+	golang.org/x/mod v0.8.0 // indirect
+	golang.org/x/net v0.6.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 144fb49b..fe91d35c 100644
--- a/go.sum
+++ b/go.sum
@@ -27,11 +27,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
-github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
-github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
 github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -78,26 +73,26 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea h1:CyhwejzVGvZ3Q2PSbQ4NRRYn+ZWv5eS1vlaEusT+bAI=
 github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea/go.mod h1:eNr558nEUjP8acGw8FFjTeWvSgU1stO7FAO6eknhHe4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
-golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
-golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c=
-google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
-google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ=
-google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
+google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
diff --git a/middleware/sampler.go b/middleware/sampler.go
index 4af61b1b..5a346aa1 100644
--- a/middleware/sampler.go
+++ b/middleware/sampler.go
@@ -32,6 +32,8 @@ const (
 	adaptiveUpperBoundFloat = float64(adaptiveUpperBoundInt)
 )
 
+var rnd = rand.New(rand.NewSource(1))
+
 // NewAdaptiveSampler computes the interval for sampling for tracing middleware.
 // it can also be used by non-web go routines to trace internal API calls.
 //
@@ -88,11 +90,11 @@ func (s *adaptiveSampler) Sample() bool {
 	}
 
 	// currentRate is never zero.
-	return currentRate == adaptiveUpperBoundInt || rand.Intn(adaptiveUpperBoundInt) < currentRate
+	return currentRate == adaptiveUpperBoundInt || rnd.Intn(adaptiveUpperBoundInt) < currentRate
 }
 
 // Sample implementation for fixed percentage
 func (s fixedSampler) Sample() bool {
 	samplingPercent := int(s)
-	return samplingPercent > 0 && (samplingPercent == 100 || rand.Intn(100) < samplingPercent)
+	return samplingPercent > 0 && (samplingPercent == 100 || rnd.Intn(100) < samplingPercent)
 }
diff --git a/pkg/error.go b/pkg/error.go
index 10f53966..e994241a 100644
--- a/pkg/error.go
+++ b/pkg/error.go
@@ -3,11 +3,10 @@ package goa
 import (
 	"crypto/rand"
 	"encoding/base64"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
-
-	"github.com/hashicorp/go-multierror"
 )
 
 type (
@@ -228,7 +227,7 @@ func MergeErrors(err, other error) error {
 	//
 	// Do this before we modify ourselves, as History() may include us!
 	e.history = append(e.History(), o.History()...)
-	e.err = multierror.Append(e.err, o.err)
+	e.err = errors.Join(e.err, o.err)
 
 	e.Message = e.Message + "; " + o.Message
 	e.Timeout = e.Timeout && o.Timeout
diff --git a/pkg/version.go b/pkg/version.go
index 3f08c93a..b862cb0d 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -12,7 +12,7 @@ const (
 	// Minor version number
 	Minor = 11
 	// Build number
-	Build = 0
+	Build = 1
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/codegen/service/testdata/views_code.go b/codegen/service/testdata/views_code.go
index 5fe6abe5..c452a91d 100644
--- a/codegen/service/testdata/views_code.go
+++ b/codegen/service/testdata/views_code.go
@@ -808,3 +808,55 @@ func ValidateUserTypeView(result UserTypeView) (err error) {
 	return
 }
 `
+
+const ResultWithPkgPathCode = `// RT is the viewed result type that is projected based on a view.
+type RT struct {
+	// Type to project
+	Projected *RTView
+	// View to render
+	View string
+}
+
+// RTView is a type that runs validations on a projected type.
+type RTView struct {
+	A *UserTypeView
+}
+
+// UserTypeView is a type that runs validations on a projected type.
+type UserTypeView struct {
+	A *string
+}
+
+var (
+	// RTMap is a map indexing the attribute names of RT by view name.
+	RTMap = map[string][]string{
+		"default": {
+			"a",
+		},
+	}
+)
+
+// ValidateRT runs the validations defined on the viewed result type RT.
+func ValidateRT(result *RT) (err error) {
+	switch result.View {
+	case "default", "":
+		err = ValidateRTView(result.Projected)
+	default:
+		err = goa.InvalidEnumValueError("view", result.View, []interface{}{"default"})
+	}
+	return
+}
+
+// ValidateRTView runs the validations defined on RTView using the "default"
+// view.
+func ValidateRTView(result *RTView) (err error) {
+
+	return
+}
+
+// ValidateUserTypeView runs the validations defined on UserTypeView.
+func ValidateUserTypeView(result *UserTypeView) (err error) {
+
+	return
+}
+`
diff --git a/codegen/service/testdata/views_dsls.go b/codegen/service/testdata/views_dsls.go
index ae5627b1..69075db3 100644
--- a/codegen/service/testdata/views_dsls.go
+++ b/codegen/service/testdata/views_dsls.go
@@ -219,7 +219,7 @@ var ResultWithMultipleMethodsDSL = func() {
 }
 
 var ResultWithEnumTypeDSL = func() {
-	var T = Type("UserType", String, func()  {
+	var T = Type("UserType", String, func() {
 		Enum("a", "b")
 	})
 	var RT = ResultType("application/vnd.result", func() {
@@ -234,6 +234,24 @@ var ResultWithEnumTypeDSL = func() {
 	})
 }
 
+var ResultWithPkgPathDSL = func() {
+	var T = Type("UserType", func() {
+		Attribute("a", String)
+		Meta("struct:pkg:path", "types")
+	})
+	var RT = ResultType("application/vnd.result", func() {
+		TypeName("RT")
+		Attributes(func() {
+			Attribute("a", T)
+		})
+	})
+	Service("ResultWithPkgPath", func() {
+		Method("A", func() {
+			Result(RT)
+		})
+	})
+}
+
 var ResultWithCustomFieldsDSL = func() {
 	var RT = ResultType("application/vnd.result", func() {
 		TypeName("RT")
diff --git a/codegen/service/views_test.go b/codegen/service/views_test.go
index 130fdd9d..3047bfe3 100644
--- a/codegen/service/views_test.go
+++ b/codegen/service/views_test.go
@@ -26,6 +26,7 @@ func TestViews(t *testing.T) {
 		{"result-with-recursive-collection-of-result-type", testdata.ResultWithRecursiveCollectionOfResultTypeDSL, testdata.ResultWithRecursiveCollectionOfResultTypeCode},
 		{"result-with-multiple-methods", testdata.ResultWithMultipleMethodsDSL, testdata.ResultWithMultipleMethodsCode},
 		{"result-with-enum-type", testdata.ResultWithEnumTypeDSL, testdata.ResultWithEnumType},
+		{"result-with-pkg-path", testdata.ResultWithPkgPathDSL, testdata.ResultWithPkgPathCode},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/middleware/sampler_test.go b/middleware/sampler_test.go
index fa68b404..2cc32e2a 100644
--- a/middleware/sampler_test.go
+++ b/middleware/sampler_test.go
@@ -1,7 +1,6 @@
 package middleware
 
 import (
-	"math/rand"
 	"testing"
 	"time"
 )
@@ -24,7 +23,7 @@ func TestFixedSampler(t *testing.T) {
 	}
 
 	// 50 %
-	rand.Seed(123) // set seed for reproducibility.
+	rnd.Seed(123) // set seed for reproducibility.
 	trueCount := 0
 	subject = NewFixedSampler(33)
 	for i := 0; i < 100; i++ {
@@ -60,7 +59,7 @@ func TestAdaptiveSampler(t *testing.T) {
 
 	// change start time to 1s ago for a more predictable result.
 	trueCount := 0
-	rand.Seed(123) // set seed for reproducibility.
+	rnd.Seed(123) // set seed for reproducibility.
 	now := time.Now()
 	subject.(*adaptiveSampler).start = now.Add(-time.Second)
 	for i := 99; i < 199; i++ {
diff --git a/pkg/error_test.go b/pkg/error_test.go
new file mode 100644
index 00000000..e8a46b60
--- /dev/null
+++ b/pkg/error_test.go
@@ -0,0 +1,44 @@
+package goa
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestServiceErrorUnwrap(t *testing.T) {
+	var (
+		errFoo          = errors.New("foo")
+		errBar          = errors.New("bar")
+		serviceErrorFoo = NewServiceError(errFoo, "foo", false, false, false)
+		serviceErrorBar = NewServiceError(errBar, "bar", false, false, false)
+	)
+	cases := map[string]struct {
+		err  error
+		want error
+	}{
+		"service error": {
+			err:  serviceErrorFoo,
+			want: errFoo,
+		},
+		"merged service error": {
+			err:  MergeErrors(serviceErrorFoo, serviceErrorBar),
+			want: errors.Join(errFoo, errBar),
+		},
+	}
+	for k, tc := range cases {
+		t.Run(k, func(t *testing.T) {
+			got := errors.Unwrap(tc.err)
+			if errs, ok := tc.want.(interface{ Unwrap() []error }); ok {
+				for _, e := range errs.Unwrap() {
+					if !errors.Is(got, e) {
+						t.Errorf("got %#v, want %#v", got, tc.want)
+					}
+				}
+			} else {
+				if !errors.Is(got, tc.want) {
+					t.Errorf("got %#v, want %#v", got, tc.want)
+				}
+			}
+		})
+	}
+}