Skip to content

Commit 39def70

Browse files
authored
fix error marshalling issues (#235)
Errors from AuthRelationshipResponse were being marshalled but due to the error interface could not be unmarshalled. This changes the type to be a new Errors type which can handle encoding and decoding errors without resulting in unmarshalling errors. The errors returned implement the error interface however they cannot be directly compared due to the errors being dynamically generated. Signed-off-by: Mike Mason <[email protected]>
1 parent e749c7e commit 39def70

File tree

2 files changed

+195
-1
lines changed

2 files changed

+195
-1
lines changed

events/message.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package events
1818
import (
1919
"context"
2020
"encoding/json"
21+
"errors"
2122
"fmt"
23+
"strings"
2224
"time"
2325

2426
"go.opentelemetry.io/otel"
@@ -303,11 +305,73 @@ func (r AuthRelationshipRelation) Validate() error {
303305
return err
304306
}
305307

308+
// Errors contains one or more errors and handles marshalling the errors.
309+
// See [Errors.MarshalJSON] and [Errors.UnmarshalJSON] for details on how marshalling is done.
310+
type Errors []error
311+
312+
// MarshalJSON encodes a string of arrays with each errors Error string.
313+
// Entries which are nil are skipped.
314+
// If no non nil errors are provided, null is returned.
315+
func (e Errors) MarshalJSON() ([]byte, error) {
316+
errs := make([]string, 0, len(e))
317+
318+
for _, err := range e {
319+
if err != nil {
320+
errs = append(errs, err.Error())
321+
}
322+
}
323+
324+
if len(errs) == 0 {
325+
return []byte("null"), nil
326+
}
327+
328+
return json.Marshal(errs)
329+
}
330+
331+
// UnmarshalJSON converts a list of string errors into new errors.
332+
// All errors unmarshalled are new errors and cannot be compared directly to another error.
333+
// Errors should be checked using string comparison.
334+
func (e *Errors) UnmarshalJSON(b []byte) error {
335+
var errs []string
336+
337+
if err := json.Unmarshal(b, &errs); err != nil {
338+
return err
339+
}
340+
341+
if len(errs) == 0 {
342+
*e = nil
343+
344+
return nil
345+
}
346+
347+
*e = make(Errors, len(errs))
348+
349+
for i, err := range errs {
350+
(*e)[i] = errors.New(err) //nolint:goerr113 // errors are dynamically returned
351+
}
352+
353+
return nil
354+
}
355+
356+
// Error returns each error on a new line.
357+
// Nil error are not included.
358+
func (e Errors) Error() string {
359+
errs := make([]string, 0, len(e))
360+
361+
for _, err := range e {
362+
if err != nil {
363+
errs = append(errs, err.Error())
364+
}
365+
}
366+
367+
return strings.Join(errs, "\n")
368+
}
369+
306370
// AuthRelationshipResponse contains the data structure expected to be received from an AuthRelationshipRequest
307371
// message to write or delete an auth relationship from PermissionsAPI
308372
type AuthRelationshipResponse struct {
309373
// Errors contains any errors, if empty the request was successful
310-
Errors []error `json:"errors"`
374+
Errors Errors `json:"errors"`
311375
// TraceContext is a map of values used for OpenTelemetry context propagation.
312376
TraceContext map[string]string `json:"traceContext"`
313377
// TraceID is the ID of the trace for this event

events/nats_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package events_test
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
7+
"os"
68
"testing"
79
"time"
810

@@ -193,6 +195,134 @@ func TestNATSRequestReply(t *testing.T) {
193195

194196
close(respGot)
195197
}
198+
func TestNATSRequestReplyMarshalling(t *testing.T) {
199+
testCases := []struct {
200+
name string
201+
input events.AuthRelationshipResponse
202+
expectDecoded map[string]any
203+
expectResponse events.AuthRelationshipResponse
204+
}{
205+
{
206+
"no error",
207+
events.AuthRelationshipResponse{
208+
TraceID: "some-id",
209+
TraceContext: map[string]string{},
210+
},
211+
map[string]any{
212+
"errors": nil,
213+
"spanID": "",
214+
"traceContext": map[string]any{},
215+
"traceID": "some-id",
216+
},
217+
events.AuthRelationshipResponse{
218+
TraceID: "some-id",
219+
TraceContext: map[string]string{},
220+
},
221+
},
222+
{
223+
"with errors",
224+
events.AuthRelationshipResponse{
225+
TraceID: "some-id",
226+
TraceContext: map[string]string{},
227+
Errors: []error{
228+
os.ErrInvalid,
229+
},
230+
},
231+
map[string]any{
232+
"errors": []any{
233+
os.ErrInvalid.Error(),
234+
},
235+
"spanID": "",
236+
"traceContext": map[string]any{},
237+
"traceID": "some-id",
238+
},
239+
events.AuthRelationshipResponse{
240+
TraceID: "some-id",
241+
TraceContext: map[string]string{},
242+
Errors: []error{
243+
errors.New(os.ErrInvalid.Error()), //nolint:goerr113 // ensure equals same error with text
244+
},
245+
},
246+
},
247+
{
248+
"nil errors skipped",
249+
events.AuthRelationshipResponse{
250+
TraceID: "some-id",
251+
TraceContext: map[string]string{},
252+
Errors: []error{
253+
os.ErrInvalid,
254+
nil,
255+
os.ErrExist,
256+
nil,
257+
},
258+
},
259+
map[string]any{
260+
"errors": []any{
261+
os.ErrInvalid.Error(),
262+
os.ErrExist.Error(),
263+
},
264+
"spanID": "",
265+
"traceContext": map[string]any{},
266+
"traceID": "some-id",
267+
},
268+
events.AuthRelationshipResponse{
269+
TraceID: "some-id",
270+
TraceContext: map[string]string{},
271+
Errors: []error{
272+
errors.New(os.ErrInvalid.Error()), //nolint:goerr113 // ensure equals same error with text
273+
errors.New(os.ErrExist.Error()), //nolint:goerr113 // ensure equals same error with text
274+
},
275+
},
276+
},
277+
{
278+
"all nil errors skipped",
279+
events.AuthRelationshipResponse{
280+
TraceID: "some-id",
281+
TraceContext: map[string]string{},
282+
Errors: []error{
283+
nil,
284+
nil,
285+
},
286+
},
287+
map[string]any{
288+
"errors": nil,
289+
"spanID": "",
290+
"traceContext": map[string]any{},
291+
"traceID": "some-id",
292+
},
293+
events.AuthRelationshipResponse{
294+
TraceID: "some-id",
295+
TraceContext: map[string]string{},
296+
Errors: nil,
297+
},
298+
},
299+
}
300+
301+
for _, tc := range testCases {
302+
tc := tc
303+
304+
t.Run(tc.name, func(t *testing.T) {
305+
encoded, err := json.Marshal(tc.input)
306+
require.NoError(t, err, "unexpected error marshalling input")
307+
308+
decoded := map[string]any{}
309+
310+
err = json.Unmarshal(encoded, &decoded)
311+
require.NoError(t, err, "unexpected error unmarshalling encoded input into map")
312+
313+
assert.Equal(t, tc.expectDecoded, decoded, "unexpected encoded response")
314+
315+
var response events.AuthRelationshipResponse
316+
317+
err = json.Unmarshal(encoded, &response)
318+
require.NoError(t, err, "unexpected error unmarshalling encoded input into response")
319+
320+
assert.Equal(t, tc.expectResponse, response, "unexpected response")
321+
322+
assert.Equal(t, len(tc.expectResponse.Errors), len(response.Errors), "unexpected response error count")
323+
})
324+
}
325+
}
196326

197327
func getSingleMessage[T any](messages <-chan events.Message[T], timeout time.Duration) (events.Message[T], error) {
198328
select {

0 commit comments

Comments
 (0)