diff --git a/message.go b/message.go index 57be999..732504b 100644 --- a/message.go +++ b/message.go @@ -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/" diff --git a/protocol/problem_report.go b/protocol/problem_report.go new file mode 100644 index 0000000..178c8d7 --- /dev/null +++ b/protocol/problem_report.go @@ -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 +} diff --git a/protocol/problem_report_test.go b/protocol/problem_report_test.go new file mode 100644 index 0000000..6bf6ea2 --- /dev/null +++ b/protocol/problem_report_test.go @@ -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) + } + + }) + } +}