-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add problem report message POC (#44)
* Add problem report message
- Loading branch information
Showing
3 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
}) | ||
} | ||
} |