Skip to content
This repository was archived by the owner on Mar 27, 2024. It is now read-only.

Commit ad977a6

Browse files
committed
feat: Add handling inbound problem report messages inside legacy-connection protocol. Add unit tests
Signed-off-by: Abdulbois <abdulbois.tursunov@avast.com>
1 parent 9fc7486 commit ad977a6

5 files changed

Lines changed: 206 additions & 9 deletions

File tree

pkg/didcomm/protocol/legacyconnection/models.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ type legacyDoc struct {
106106
Proof []interface{} `json:"proof,omitempty"`
107107
}
108108

109+
type problemReport struct {
110+
Type string `json:"@type,omitempty"`
111+
ID string `json:"@id,omitempty"`
112+
Thread *decorator.Thread `json:"~thread,omitempty"`
113+
ProblemCode string `json:"problem-code,omitempty"`
114+
Explain string `json:"explain,omitempty"`
115+
Localization struct {
116+
Locale string `json:"locale,omitempty"`
117+
} `json:"~l10n,omitempty"`
118+
}
119+
109120
// JSONBytes converts Connection to json bytes.
110121
func (con *Connection) toLegacyJSONBytes() ([]byte, error) {
111122
if con.DIDDoc == nil {

pkg/didcomm/protocol/legacyconnection/service.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ const (
4545
// ResponseMsgType defines the legacy-connection response message type.
4646
ResponseMsgType = PIURI + "/response"
4747
// AckMsgType defines the legacy-connection ack message type.
48-
AckMsgType = "https://didcomm.org/notification/1.0/ack"
48+
AckMsgType = "https://didcomm.org/notification/1.0/ack"
49+
// ProblemReportMsgType defines the protocol problem-report message type.
50+
ProblemReportMsgType = PIURI + "/problem-report"
4951
routerConnsMetadataKey = "routerConnections"
5052
)
5153

@@ -295,7 +297,8 @@ func (s *Service) Accept(msgType string) bool {
295297
return msgType == InvitationMsgType ||
296298
msgType == RequestMsgType ||
297299
msgType == ResponseMsgType ||
298-
msgType == AckMsgType
300+
msgType == AckMsgType ||
301+
msgType == ProblemReportMsgType
299302
}
300303

301304
// HandleOutbound handles outbound connection messages.
@@ -318,6 +321,10 @@ func (s *Service) nextState(msgType, thID string) (state, error) {
318321

319322
logger.Debugf("retrieved current state [%s] using nsThID [%s]", current.Name(), nsThID)
320323

324+
if msgType == ProblemReportMsgType {
325+
return &responded{}, nil
326+
}
327+
321328
next, err := stateFromMsgType(msgType)
322329
if err != nil {
323330
return nil, err
@@ -636,7 +643,7 @@ func (s *Service) connectionRecord(msg service.DIDCommMsg, ctx service.DIDCommCo
636643
return s.requestMsgRecord(msg, ctx)
637644
case ResponseMsgType:
638645
return s.responseMsgRecord(msg)
639-
case AckMsgType:
646+
case AckMsgType, ProblemReportMsgType:
640647
return s.fetchConnectionRecord(theirNSPrefix, msg)
641648
}
642649

pkg/didcomm/protocol/legacyconnection/service_test.go

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func TestService_Handle_Inviter(t *testing.T) {
176176
require.NoError(t, err)
177177

178178
completedFlag := make(chan struct{})
179-
respondedFlag := make(chan struct{})
179+
respondedFlag := make(chan string)
180180

181181
go msgEventListener(t, statusCh, respondedFlag, completedFlag)
182182

@@ -247,7 +247,153 @@ func TestService_Handle_Inviter(t *testing.T) {
247247
validateState(t, s, thid, findNamespace(AckMsgType), (&completed{}).Name())
248248
}
249249

250-
func msgEventListener(t *testing.T, statusCh chan service.StateMsg, respondedFlag, completedFlag chan struct{}) {
250+
func TestService_Handle_Inviter_With_ProblemReport(t *testing.T) {
251+
mockStore := &mockstorage.MockStore{Store: make(map[string]mockstorage.DBEntry)}
252+
storeProv := mockstorage.NewCustomMockStoreProvider(mockStore)
253+
k := newKMS(t, storeProv)
254+
prov := &protocol.MockProvider{
255+
StoreProvider: storeProv,
256+
ServiceMap: map[string]interface{}{
257+
mediator.Coordination: &mockroute.MockMediatorSvc{},
258+
},
259+
CustomKMS: k,
260+
KeyTypeValue: kms.ED25519Type,
261+
KeyAgreementTypeValue: kms.X25519ECDHKWType,
262+
}
263+
264+
ctx := &context{
265+
outboundDispatcher: prov.OutboundDispatcher(),
266+
crypto: &tinkcrypto.Crypto{},
267+
kms: k,
268+
keyType: kms.ED25519Type,
269+
keyAgreementType: kms.X25519ECDHKWType,
270+
}
271+
272+
_, pubKey, err := ctx.kms.CreateAndExportPubKeyBytes(kms.ED25519Type)
273+
require.NoError(t, err)
274+
275+
ctx.vdRegistry = &mockvdr.MockVDRegistry{CreateValue: createDIDDocWithKey(pubKey)}
276+
277+
connRec, err := connection.NewRecorder(prov)
278+
require.NoError(t, err)
279+
require.NotNil(t, connRec)
280+
281+
ctx.connectionRecorder = connRec
282+
283+
doc, err := ctx.vdRegistry.Create(testMethod, nil)
284+
require.NoError(t, err)
285+
286+
s, err := New(prov)
287+
require.NoError(t, err)
288+
289+
actionCh := make(chan service.DIDCommAction, 10)
290+
err = s.RegisterActionEvent(actionCh)
291+
require.NoError(t, err)
292+
293+
statusCh := make(chan service.StateMsg, 10)
294+
err = s.RegisterMsgEvent(statusCh)
295+
require.NoError(t, err)
296+
297+
completedFlag := make(chan struct{})
298+
respondedFlag := make(chan string)
299+
300+
go msgEventListener(t, statusCh, respondedFlag, completedFlag)
301+
go func() { service.AutoExecuteActionEvent(actionCh) }()
302+
303+
invitation := &Invitation{
304+
Type: InvitationMsgType,
305+
ID: randomString(),
306+
Label: "Bob",
307+
RecipientKeys: []string{base58.Encode(pubKey)},
308+
ServiceEndpoint: "http://alice.agent.example.com:8081",
309+
}
310+
311+
err = ctx.connectionRecorder.SaveInvitation(invitation.ID, invitation)
312+
require.NoError(t, err)
313+
314+
thid := randomString()
315+
316+
// Invitation was previously sent by Alice to Bob.
317+
// Bob now sends a connection Request
318+
connRequest, err := json.Marshal(
319+
&Request{
320+
Type: RequestMsgType,
321+
ID: thid,
322+
Label: "Bob",
323+
Thread: &decorator.Thread{
324+
PID: invitation.ID,
325+
},
326+
Connection: &Connection{
327+
DID: doc.DIDDocument.ID,
328+
DIDDoc: doc.DIDDocument,
329+
},
330+
})
331+
require.NoError(t, err)
332+
requestMsg, err := service.ParseDIDCommMsgMap(connRequest)
333+
require.NoError(t, err)
334+
_, err = s.HandleInbound(requestMsg, service.NewDIDCommContext(doc.DIDDocument.ID, "", nil))
335+
require.NoError(t, err)
336+
337+
var connID string
338+
select {
339+
case connID = <-respondedFlag:
340+
case <-time.After(2 * time.Second):
341+
require.Fail(t, "didn't receive connection ID")
342+
}
343+
344+
connRecord, err := s.connectionRecorder.GetConnectionRecord(connID)
345+
require.NoError(t, err)
346+
347+
// Alice automatically sends connection Response to Bob
348+
// Bob replies with Problem Report
349+
prbRpt, err := json.Marshal(
350+
&problemReport{
351+
ID: randomString(),
352+
Type: ProblemReportMsgType,
353+
Thread: &decorator.Thread{ID: connRecord.ThreadID},
354+
})
355+
require.NoError(t, err)
356+
357+
prbRptMsg, err := service.ParseDIDCommMsgMap(prbRpt)
358+
require.NoError(t, err)
359+
360+
_, err = s.HandleInbound(prbRptMsg, service.NewDIDCommContext(doc.DIDDocument.ID, "", nil))
361+
require.NoError(t, err)
362+
363+
validateState(t, s, thid, findNamespace(RequestMsgType), (&responded{}).Name())
364+
365+
_, err = ctx.connectionRecorder.GetConnectionRecord(connID)
366+
require.ErrorContains(t, err, "data not found")
367+
368+
_, err = s.HandleInbound(requestMsg, service.NewDIDCommContext(doc.DIDDocument.ID, "", nil))
369+
require.NoError(t, err)
370+
371+
// Finally Bob replies with an ACK
372+
ack, err := json.Marshal(
373+
&model.Ack{
374+
Type: AckMsgType,
375+
ID: randomString(),
376+
Status: "OK",
377+
Thread: &decorator.Thread{ID: connRecord.ThreadID},
378+
})
379+
require.NoError(t, err)
380+
381+
ackMsg, err := service.ParseDIDCommMsgMap(ack)
382+
require.NoError(t, err)
383+
384+
_, err = s.HandleInbound(ackMsg, service.NewDIDCommContext(doc.DIDDocument.ID, "", nil))
385+
require.NoError(t, err)
386+
387+
select {
388+
case <-completedFlag:
389+
case <-time.After(4 * time.Second):
390+
require.Fail(t, "didn't receive post event complete")
391+
}
392+
393+
validateState(t, s, connRecord.ThreadID, findNamespace(AckMsgType), (&completed{}).Name())
394+
}
395+
396+
func msgEventListener(t *testing.T, statusCh chan service.StateMsg, respondedFlag chan string, completedFlag chan struct{}) { //nolint: lll
251397
for e := range statusCh {
252398
require.Equal(t, LegacyConnection, e.ProtocolName)
253399

@@ -272,11 +418,11 @@ func msgEventListener(t *testing.T, statusCh chan service.StateMsg, respondedFla
272418
close(completedFlag)
273419
}
274420

275-
if e.StateID == "responded" {
421+
if e.StateID == "responded" && e.Msg.Type() != ProblemReportMsgType {
276422
// validate connectionID received during state transition with original connectionID
277423
require.NotNil(t, prop.ConnectionID())
278424
require.NotNil(t, prop.InvitationID())
279-
close(respondedFlag)
425+
respondedFlag <- prop.ConnectionID()
280426
}
281427
}
282428
}

pkg/didcomm/protocol/legacyconnection/states.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func (s *responded) Name() string {
204204
}
205205

206206
func (s *responded) CanTransitionTo(next state) bool {
207-
return StateIDCompleted == next.Name()
207+
return StateIDCompleted == next.Name() || StateIDRequested == next.Name()
208208
}
209209

210210
func (s *responded) ExecuteInbound(msg *stateMachineMsg, _ string, ctx *context) (*connectionstore.Record,
@@ -226,6 +226,13 @@ func (s *responded) ExecuteInbound(msg *stateMachineMsg, _ string, ctx *context)
226226
return connRecord, &noOp{}, action, nil
227227
case ResponseMsgType:
228228
return msg.connRecord, &completed{}, func() error { return nil }, nil
229+
case ProblemReportMsgType:
230+
err := ctx.connectionRecorder.RemoveConnection(msg.connRecord.ConnectionID)
231+
if err != nil {
232+
return nil, nil, nil, fmt.Errorf("delete connection record is failed: %w", err)
233+
}
234+
235+
return msg.connRecord, &noOp{}, func() error { return nil }, nil
229236
default:
230237
return nil, nil, nil, fmt.Errorf("illegal msg type %s for state %s", msg.Type(), s.Name())
231238
}

pkg/didcomm/protocol/legacyconnection/states_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func TestRespondedState(t *testing.T) {
9292
require.Equal(t, "responded", res.Name())
9393
require.False(t, res.CanTransitionTo(&null{}))
9494
require.False(t, res.CanTransitionTo(&invited{}))
95-
require.False(t, res.CanTransitionTo(&requested{}))
95+
require.True(t, res.CanTransitionTo(&requested{}))
9696
require.False(t, res.CanTransitionTo(res))
9797
require.True(t, res.CanTransitionTo(&completed{}))
9898
}
@@ -388,6 +388,32 @@ func TestRespondedState_Execute(t *testing.T) {
388388
require.NotNil(t, connRec)
389389
require.Equal(t, (&completed{}).Name(), followup.Name())
390390
})
391+
t.Run("followup to 'noop' on inbound problem report message", func(t *testing.T) {
392+
connRec := &connection.Record{
393+
State: (&responded{}).Name(),
394+
ThreadID: request.ID,
395+
ConnectionID: "123",
396+
Namespace: findNamespace(ResponseMsgType),
397+
}
398+
err = ctx.connectionRecorder.SaveConnectionRecordWithMappings(connRec)
399+
require.NoError(t, err)
400+
401+
problemReportPayload, err := json.Marshal(&problemReport{Type: ProblemReportMsgType})
402+
require.NoError(t, err)
403+
404+
connRec, followup, _, e := (&responded{}).ExecuteInbound(
405+
&stateMachineMsg{
406+
DIDCommMsg: bytesToDIDCommMsg(t, problemReportPayload),
407+
connRecord: connRec,
408+
}, "", ctx)
409+
require.NoError(t, e)
410+
require.NotNil(t, connRec)
411+
412+
_, e = ctx.connectionRecorder.GetConnectionRecord(connRec.ConnectionID)
413+
require.Error(t, e)
414+
require.ErrorContains(t, e, "data not found")
415+
require.Equal(t, (&noOp{}).Name(), followup.Name())
416+
})
391417

392418
t.Run("handle inbound request unmarshalling error", func(t *testing.T) {
393419
_, followup, _, err := (&responded{}).ExecuteInbound(&stateMachineMsg{

0 commit comments

Comments
 (0)