package presenter_test import ( "bytes" "context" "fmt" "log/slog" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/vektah/gqlparser/v2/gqlerror" "gitea.unbound.se/shiny/presenter" ) func Test_globalErrorPresenter(t *testing.T) { type args struct { ctx context.Context err error } tests := []struct { name string args args want *gqlerror.Error logged []string }{ { name: "coded error", args: args{ ctx: context.Background(), err: ErrEntryNotFound, }, want: wrap(ErrEntryNotFound, Code("NOT_FOUND"), ErrorEntity("ENTRY")), logged: []string{"level=ERROR msg=\"entry not found\""}, }, { name: "coded error with param", args: args{ ctx: context.Background(), err: ErrEntryNotFound.WithParam("email", "jim@example.org").WithParam("other", "stuff"), }, want: wrap(ErrEntryNotFound.WithParam("email", "jim@example.org").WithParam("other", "stuff"), Code("NOT_FOUND"), ErrorEntity("ENTRY"), Param("email", "jim@example.org"), Param("other", "stuff")), logged: []string{"level=ERROR msg=\"entry not found\""}, }, { name: "unknown code", args: args{ ctx: context.Background(), err: presenter.NewCodedError("unknown code", "UNKNOWN", presenter.EntityEntry), }, want: wrap(presenter.NewCodedError("unknown code", "UNKNOWN", presenter.EntityEntry), Code("INTERNAL"), ErrorEntity("ENTRY")), logged: []string{"level=ERROR msg=\"unknown code\""}, }, { name: "unknown entity", args: args{ ctx: context.Background(), err: presenter.NewCodedError("unknown entity", presenter.CodeNotFound, "UNKNOWN"), }, want: wrap(presenter.NewCodedError("unknown entity", presenter.CodeNotFound, "UNKNOWN"), Code("NOT_FOUND")), logged: []string{"level=ERROR msg=\"unknown entity\""}, }, { name: "unhandled error", args: args{ ctx: context.Background(), err: fmt.Errorf("unhandled"), }, want: wrap(fmt.Errorf("unhandled"), Code("INTERNAL")), logged: []string{"level=ERROR msg=unhandled"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger := newMockLogger() got := presenter.New(logger.Logger(), AllErrorCode, AllTestErrorEntity, ErrorCodeInternal)(tt.args.ctx, tt.args.err) assert.Equal(t, tt.want, got) logger.Check(t, tt.logged) }) } } type ErrorOpt = func(err *gqlerror.Error) func Code(code ErrorCode) ErrorOpt { return func(err *gqlerror.Error) { err.Extensions["code"] = code } } func ErrorEntity(entity TestErrorEntity) ErrorOpt { return func(err *gqlerror.Error) { err.Extensions["errorEntity"] = entity } } func Param(key, value string) ErrorOpt { return func(err *gqlerror.Error) { if e, exists := err.Extensions["params"]; !exists { params := make(map[string]string) params[key] = value err.Extensions["params"] = params } else { e.(map[string]string)[key] = value } } } func wrap(err error, opts ...ErrorOpt) *gqlerror.Error { e := gqlerror.WrapPath(nil, err) e.Extensions = map[string]interface{}{} for _, o := range opts { o(e) } return e } func newMockLogger() *mockLogger { logged := &bytes.Buffer{} return &mockLogger{ logged: logged, logger: slog.New(slog.NewTextHandler(logged, &slog.HandlerOptions{ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { if a.Key == "time" { return slog.Attr{} } return a }, })), } } type mockLogger struct { logger *slog.Logger logged *bytes.Buffer } func (m *mockLogger) Logger() *slog.Logger { return m.logger } func (m *mockLogger) Check(t testing.TB, wantLogged []string) { var gotLogged []string if m.logged.String() != "" { gotLogged = strings.Split(m.logged.String(), "\n") gotLogged = gotLogged[:len(gotLogged)-1] } if len(wantLogged) == 0 { assert.Empty(t, gotLogged) return } assert.Equal(t, wantLogged, gotLogged) } type ErrorCode string const ( ErrorCodeNotFound ErrorCode = "NOT_FOUND" ErrorCodeConflict ErrorCode = "CONFLICT" ErrorCodePreconditionFailed ErrorCode = "PRECONDITION_FAILED" ErrorCodeInternal ErrorCode = "INTERNAL" ) var AllErrorCode = []ErrorCode{ ErrorCodeNotFound, ErrorCodeConflict, ErrorCodePreconditionFailed, ErrorCodeInternal, } type TestErrorEntity string const ( TestErrorEntityEntrySeries TestErrorEntity = "ENTRY_SERIES" TestErrorEntityFiscalYear TestErrorEntity = "FISCAL_YEAR" TestErrorEntityAccountClass TestErrorEntity = "ACCOUNT_CLASS" TestErrorEntityAccountGroup TestErrorEntity = "ACCOUNT_GROUP" TestErrorEntityAccount TestErrorEntity = "ACCOUNT" TestErrorEntityTag TestErrorEntity = "TAG" TestErrorEntityEntry TestErrorEntity = "ENTRY" TestErrorEntityEntryBasis TestErrorEntity = "ENTRY_BASIS" ) var AllTestErrorEntity = []TestErrorEntity{ TestErrorEntityEntrySeries, TestErrorEntityFiscalYear, TestErrorEntityAccountClass, TestErrorEntityAccountGroup, TestErrorEntityAccount, TestErrorEntityTag, TestErrorEntityEntry, TestErrorEntityEntryBasis, }