Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,31 +141,38 @@ func (e TrogonError) Error() string {
}

if len(e.metadata) > 0 {
sb.WriteString("\n metadata:\n")
sb.WriteString("\n metadata:")

for _, k := range slices.Sorted(maps.Keys(e.metadata)) {
v := e.metadata[k]
fmt.Fprintf(sb, " - %s: %s visibility=%s\n", k, v.value, v.visibility.String())
fmt.Fprintf(sb, "\n - %s: %s visibility=%s", k, v.value, v.visibility.String())
}
}

if e.help != nil && len(e.help.links) > 0 {
sb.WriteString("\n")
sb.WriteString("\n\n")
for i, link := range e.help.links {
if i > 0 {
sb.WriteString("\n")
}
fmt.Fprintf(sb, "- %s: %s", link.description, link.url)
}
sb.WriteString("\n\n")
}

if e.wrappedErr != nil {
sb.WriteString("\n\nwrapped error: ")
sb.WriteString(e.wrappedErr.Error())
}

if e.debugInfo != nil {
sb.WriteString("\n")
if e.debugInfo.detail != "" {
sb.WriteString("\n")
sb.WriteString(e.debugInfo.detail)
}

for _, entry := range e.debugInfo.StackEntries() {
sb.WriteString("\n")
sb.WriteString(entry)
}
}
Expand Down
154 changes: 139 additions & 15 deletions error_format_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package trogonerror_test

import (
"errors"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -35,8 +36,7 @@ func TestTrogonError_ExactFormat_WithRetryDuration(t *testing.T) {
code: RESOURCE_EXHAUSTED
retryInfo: retryOffset=1m0s
metadata:
- limit: 1000 visibility=PUBLIC
`
- limit: 1000 visibility=PUBLIC`

assert.Equal(t, expected, err.Error())
}
Expand All @@ -55,18 +55,17 @@ func TestTrogonError_ExactFormat_MetadataOrdering(t *testing.T) {
metadata:
- customerId: gid://shopify/Customer/1234567890 visibility=PUBLIC
- orderId: gid://shopify/Order/5432109876 visibility=PUBLIC
- productType: digital visibility=PUBLIC
`
- productType: digital visibility=PUBLIC`

assert.Equal(t, expected, err.Error())
}

func TestTrogonError_ExactFormat_EmptyOptionalFields(t *testing.T) {
err := trogonerror.NewError("shopify.core", "RESOURCE_MISSING",
trogonerror.WithCode(trogonerror.CodeNotFound),
trogonerror.WithSubject(""), // Empty string
trogonerror.WithID(""), // Empty string
trogonerror.WithSourceID("")) // Empty string
trogonerror.WithSubject(""),
trogonerror.WithID(""),
trogonerror.WithSourceID(""))

expected := `resource not found
visibility: INTERNAL
Expand All @@ -88,11 +87,10 @@ func TestTrogonError_ExactFormat_MultipleHelpLinks(t *testing.T) {
domain: shopify.support
reason: HELP_SYSTEM_DOWN
code: UNKNOWN

- Contact Support: https://admin.shopify.com/support/new
- Check Status: https://status.shopify.com
- Retry Request: https://admin.shopify.com/support/retry

`
- Retry Request: https://admin.shopify.com/support/retry`

assert.Equal(t, expected, err.Error())
}
Expand Down Expand Up @@ -155,9 +153,7 @@ func TestTrogonError_ExactFormat_WithAllOptionalFields(t *testing.T) {
- amount: 299.99 visibility=PRIVATE
- currency: USD visibility=PUBLIC

- Retry Payment: https://admin.shopify.com/orders/pay_2024_01_15_def456ghi789/retry

`
- Retry Payment: https://admin.shopify.com/orders/pay_2024_01_15_def456ghi789/retry`

assert.Equal(t, expected, err.Error())
}
Expand All @@ -173,14 +169,15 @@ func TestTrogonError_ExactFormat_WithStackTrace(t *testing.T) {
visibility: INTERNAL
domain: shopify.debugging
reason: STACK_TRACE_ERROR
code: UNKNOWNCustom debug detail`
code: UNKNOWN

Custom debug detail`
assert.True(t, strings.HasPrefix(errorOutput, expectedPrefix))
assert.Contains(t, errorOutput, ".go:")
assert.Contains(t, errorOutput, "github.com/TrogonStack/trogonerror")
}

func TestTrogonError_ExactFormat_CompleteErrorWithStackTrace(t *testing.T) {
// Create a comprehensive error to test the complete format
timestamp := time.Date(2024, 1, 15, 14, 30, 45, 0, time.UTC)
retryTime := time.Date(2024, 1, 15, 14, 35, 45, 0, time.UTC)

Expand Down Expand Up @@ -231,3 +228,130 @@ Payment gateway integration failure: upstream timeout`
assert.Contains(t, errorOutput, ".go:")
assert.Contains(t, errorOutput, "github.com/TrogonStack/trogonerror")
}

func TestTrogonError_ExactFormat_WithWrappedStandardError(t *testing.T) {
originalErr := errors.New("connection timeout after 30s")
err := trogonerror.NewError("myapp.database", "CONNECTION_FAILED",
trogonerror.WithCode(trogonerror.CodeUnavailable),
trogonerror.WithMessage("Failed to connect to database"),
trogonerror.WithWrap(originalErr))

expected := `Failed to connect to database
visibility: INTERNAL
domain: myapp.database
reason: CONNECTION_FAILED
code: UNAVAILABLE

wrapped error: connection timeout after 30s`

assert.Equal(t, expected, err.Error())
}

func TestTrogonError_ExactFormat_WithWrappedTrogonError(t *testing.T) {
innerErr := trogonerror.NewError("myapp.network", "DNS_RESOLUTION_FAILED",
trogonerror.WithCode(trogonerror.CodeInternal),
trogonerror.WithMessage("DNS lookup failed for db.example.com"))

outerErr := trogonerror.NewError("myapp.database", "CONNECTION_FAILED",
trogonerror.WithCode(trogonerror.CodeUnavailable),
trogonerror.WithMessage("Database connection establishment failed"),
trogonerror.WithWrap(innerErr))

expected := `Database connection establishment failed
visibility: INTERNAL
domain: myapp.database
reason: CONNECTION_FAILED
code: UNAVAILABLE

wrapped error: DNS lookup failed for db.example.com
visibility: INTERNAL
domain: myapp.network
reason: DNS_RESOLUTION_FAILED
code: INTERNAL`

assert.Equal(t, expected, outerErr.Error())
}

func TestTrogonError_ExactFormat_NoWrappedError(t *testing.T) {
err := trogonerror.NewError("myapp.validation", "INVALID_INPUT",
trogonerror.WithCode(trogonerror.CodeInvalidArgument),
trogonerror.WithMessage("Email format is invalid"))

expected := `Email format is invalid
visibility: INTERNAL
domain: myapp.validation
reason: INVALID_INPUT
code: INVALID_ARGUMENT`

assert.Equal(t, expected, err.Error())
}

func TestTrogonError_ExactFormat_WrappedErrorWithMetadata(t *testing.T) {
originalErr := errors.New("invalid JSON at position 42")
err := trogonerror.NewError("myapp.parser", "PARSE_ERROR",
trogonerror.WithCode(trogonerror.CodeInvalidArgument),
trogonerror.WithMessage("Failed to parse request body"),
trogonerror.WithMetadataValue(trogonerror.VisibilityPublic, "contentType", "application/json"),
trogonerror.WithMetadataValue(trogonerror.VisibilityPrivate, "position", "42"),
trogonerror.WithWrap(originalErr))

expected := `Failed to parse request body
visibility: INTERNAL
domain: myapp.parser
reason: PARSE_ERROR
code: INVALID_ARGUMENT
metadata:
- contentType: application/json visibility=PUBLIC
- position: 42 visibility=PRIVATE

wrapped error: invalid JSON at position 42`

assert.Equal(t, expected, err.Error())
}

func TestTrogonError_ExactFormat_WrappedErrorWithAllFields(t *testing.T) {
timestamp := time.Date(2024, 1, 15, 14, 30, 45, 0, time.UTC)
originalErr := errors.New("network connection reset")

err := trogonerror.NewError("myapp.service", "SERVICE_ERROR",
trogonerror.WithCode(trogonerror.CodeInternal),
trogonerror.WithMessage("Service request failed"),
trogonerror.WithVisibility(trogonerror.VisibilityPrivate),
trogonerror.WithID("err_2024_01_15_svc_123"),
trogonerror.WithTime(timestamp),
trogonerror.WithSubject("/api/endpoint"),
trogonerror.WithSourceID("service-node-01"),
trogonerror.WithHelpLink("Retry Request", "https://docs.example.com/retry"),
trogonerror.WithWrap(originalErr))

expected := `Service request failed
visibility: PRIVATE
domain: myapp.service
reason: SERVICE_ERROR
code: INTERNAL
id: err_2024_01_15_svc_123
time: 2024-01-15T14:30:45Z
subject: /api/endpoint
sourceId: service-node-01

- Retry Request: https://docs.example.com/retry

wrapped error: network connection reset`

assert.Equal(t, expected, err.Error())
}

func TestTrogonError_ExactFormat_WrappedErrorBeforeStackTrace(t *testing.T) {
originalErr := errors.New("database connection timeout")
err := trogonerror.NewError("myapp.service", "OPERATION_FAILED",
trogonerror.WithCode(trogonerror.CodeInternal),
trogonerror.WithMessage("Service operation failed"),
trogonerror.WithWrap(originalErr),
trogonerror.WithStackTrace(),
trogonerror.WithDebugDetail("Debug: Connection pool exhausted"))

output := err.Error()

assert.Contains(t, output, "wrapped error: database connection timeout")
assert.True(t, strings.Index(output, "wrapped error:") < strings.Index(output, "Debug: Connection pool exhausted"))
}
3 changes: 0 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ func ExampleNewError() {
// code: NOT_FOUND
// metadata:
// - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC
//
// shopify.users
// NOT_FOUND
}
Expand All @@ -46,7 +45,6 @@ func ExampleErrorTemplate() {
// code: NOT_FOUND
// metadata:
// - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC
//
// shopify.users
// NOT_FOUND
}
Expand Down Expand Up @@ -102,7 +100,6 @@ func ExampleNewError_basic() {
// code: NOT_FOUND
// metadata:
// - userId: gid://shopify/Customer/1234567890 visibility=PUBLIC
//
// Domain: shopify.users
// Reason: NOT_FOUND
// Code: NOT_FOUND
Expand Down
2 changes: 0 additions & 2 deletions template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ func ExampleErrorTemplate_reusable() {
// subject: /email
// metadata:
// - fieldName: email visibility=PUBLIC
//
// Phone validation: invalid argument provided
// visibility: PUBLIC
// domain: shopify.validation
Expand All @@ -265,7 +264,6 @@ func ExampleErrorTemplate_reusable() {
// subject: /phone
// metadata:
// - fieldName: phone visibility=PUBLIC
//
// Same domain: true
// Same reason: true
}