Skip to content

Commit

Permalink
Add problem report message POC (#44)
Browse files Browse the repository at this point in the history
* Add problem report message
  • Loading branch information
x1m3 authored Apr 1, 2024
1 parent 61b3fae commit dab9166
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
3 changes: 3 additions & 0 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ type ProtocolMessage string

// Iden3Protocol is a const for protocol definition
const Iden3Protocol = "https://iden3-communication.io/"

// DidCommProtocol is a const for didcomm protocol definition
const DidCommProtocol = "https://didcomm.org/"
118 changes: 118 additions & 0 deletions protocol/problem_report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package protocol

import (
"strings"

"github.com/iden3/iden3comm/v2"
"github.com/pkg/errors"
)

const (
// ProblemReportMessageType is type for problem report
ProblemReportMessageType iden3comm.ProtocolMessage = iden3comm.DidCommProtocol + "report-problem/2.0/problem-report"

// ProblemReportTypeError is type for error problem report
ProblemReportTypeError = "e"

// ProblemReportTypeWarning is type for error problem report
ProblemReportTypeWarning = "w"

// ReportDescriptorTrust - Failed to achieve required trust.
ReportDescriptorTrust = "trust"

// ReportDescriptorTrustCrypto - Cryptographic operation failed.
ReportDescriptorTrustCrypto = "trust.crypto"

// ReportDescriptorTransport - Unable to transport data
ReportDescriptorTransport = "xfer"

// ReportDescriptorDID - DID is unusable
ReportDescriptorDID = "did"

// ReportDescriptorMsg - Bad message
ReportDescriptorMsg = "msg"

// ReportDescriptorMe - Internal error
ReportDescriptorMe = "me"

// ReportDescriptorReq - Circumstances don’t satisfy requirements. Request cannot be processed because circumstances has changed
ReportDescriptorReq = "req"

// ReportDescriptorReqTime - Failed to satisfy timing constraints.
ReportDescriptorReqTime = "req.time"

// ReportDescriptorLegal - Failed for legal reasons.
ReportDescriptorLegal = "legal"
)

// ProblemReportMessage represent Iden3Message for problem report
type ProblemReportMessage struct {
ID string `json:"id"`
Typ iden3comm.MediaType `json:"typ,omitempty"`
Type iden3comm.ProtocolMessage `json:"type"`
ThreadID string `json:"thid,omitempty"`
ParentThreadID string `json:"pthid"`
Ack []string `json:"ack,omitempty"`

Body ProblemReportMessageBody `json:"body,omitempty"`

From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
}

// ProblemReportMessageBody is struct the represents body for problem report
// Code is an error code. Example
// Comment is a human-readable description of the problem. Directly related to the error code.
// Args is a list of strings that can be used to replace placeholders in the error message.
// EscalateTo is a string that can be used to escalate the problem to a human operator. It can be an email
type ProblemReportMessageBody struct {
Code ProblemErrorCode `json:"code"`
Comment string `json:"comment,omitempty"`
Args []string `json:"args,omitempty"`
EscalateTo string `json:"escalate_to,omitempty"`
}

// ProblemErrorCode is a string that represents an error code "e.p.xxxx.yyyy.zzzz"
type ProblemErrorCode string

// NewProblemReportErrorCode is a helper function to create a valid ProblemErrorCode
func NewProblemReportErrorCode(sorter, scope string, descriptors []string) (ProblemErrorCode, error) {
if sorter != "e" && sorter != "w" {
return "", errors.New("invalid sorter. allowed values [e:error, w:warning]")
}
if !isKebabCase(scope) {
return "", errors.New("invalid scope. must be kebab-case")
}
if len(descriptors) == 0 {
return "", errors.New("at least one descriptor is required")
}
for _, d := range descriptors {
if !isKebabCase(d) {
return "", errors.New("invalid descriptor. must be kebab-case")
}
}
return ProblemErrorCode(sorter + "." + scope + "." + strings.Join(descriptors, ".")), nil
}

// ParseProblemErrorCode parses a string into a ProblemErrorCode. Useful to validate strings from external sources
func ParseProblemErrorCode(s string) (ProblemErrorCode, error) {
parts := strings.Split(s, ".")
if len(parts) < 3 {
return "", errors.New("invalid error code. format sorter.scope.descriptors")
}
return NewProblemReportErrorCode(parts[0], parts[1], parts[2:])
}

func isKebabCase(s string) bool {
for i, r := range s {
if r == '-' {
if i == 0 || i == len(s)-1 {
return false
}
if s[i-1] == '-' {
return false
}
}
}
return true
}
137 changes: 137 additions & 0 deletions protocol/problem_report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package protocol

import (
"testing"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

func TestNewProblemReportErrorCode(t *testing.T) {
type expected struct {
code ProblemErrorCode
err error
}
for _, tc := range []struct {
desc string
sorter string
scope string
descriptors []string
expected expected
}{
{
desc: "Sorter should be e or w",
sorter: "x",
scope: "scope",
expected: expected{
err: errors.New("invalid sorter. allowed values [e:error, w:warning]"),
},
},
{
desc: "At lease one descriptor is required",
sorter: ProblemReportTypeError,
scope: "scope",
expected: expected{
err: errors.New("at least one descriptor is required"),
},
},
{
desc: "Scope must be kebab-case 1",
sorter: ProblemReportTypeError,
scope: "scope-",
expected: expected{
err: errors.New("invalid scope. must be kebab-case"),
},
},
{
desc: "Scope must be kebab-case 2",
sorter: ProblemReportTypeWarning,
scope: "-scope",
expected: expected{
err: errors.New("invalid scope. must be kebab-case"),
},
},
{
desc: "Scope must be kebab-case 3",
sorter: ProblemReportTypeError,
scope: "a---scope",
expected: expected{
err: errors.New("invalid scope. must be kebab-case"),
},
},
{
desc: "Happy path, one descriptor",
sorter: ProblemReportTypeWarning,
scope: ReportDescriptorTransport,
descriptors: []string{"remote-server-down"},
expected: expected{
code: "w.xfer.remote-server-down",
},
},
{
desc: "Happy path, multiple descriptors",
sorter: ProblemReportTypeError,
scope: ReportDescriptorTransport,
descriptors: []string{"cant-use-endpoint", "dns-failed"},
expected: expected{
code: "e.xfer.cant-use-endpoint.dns-failed",
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
c, err := NewProblemReportErrorCode(tc.sorter, tc.scope, tc.descriptors)
if tc.expected.err != nil {
assert.Equal(t, err.Error(), tc.expected.err.Error())
}
assert.Equal(t, c, tc.expected.code)
})
}
}

func TestParseProblemErrorCode(t *testing.T) {
for _, tc := range []struct {
desc string
code string
descriptors []string
err error
}{
{
desc: "Empty code",
code: "",
err: errors.New("invalid error code. format sorter.scope.descriptors"),
},
{
desc: "One field",
code: ProblemReportTypeWarning,
err: errors.New("invalid error code. format sorter.scope.descriptors"),
},
{
desc: "No descriptor",
code: ProblemReportTypeWarning + ReportDescriptorTransport,
err: errors.New("invalid error code. format sorter.scope.descriptors"),
},
{
desc: "Happy Path, one descriptor",
code: "w.xfer.remote-server-down",
err: nil,
},
{
desc: "Happy Path, multiple descriptors",
code: "w.xfer.remote-server-down.desc2.desc3.descN",
err: nil,
},
} {
t.Run(tc.desc, func(t *testing.T) {
c, err := ParseProblemErrorCode(tc.code)
if tc.err != nil {
assert.Equal(t, err.Error(), tc.err.Error())
assert.Empty(t, c)

} else {
assert.NoError(t, err)
assert.Equal(t, ProblemErrorCode(tc.code), c)
}

})
}
}

0 comments on commit dab9166

Please sign in to comment.