Skip to content

Commit

Permalink
modify care team confirmation context for alerting
Browse files Browse the repository at this point in the history
Care team invitation/confirmation contexts are currently a
clients.Permissions object. To support alerting, the context is extended to
include a models.AlertsConfig.

The custom JSON unmarshaler allows flexibility in parsing the following
received contexts:

    1. The existing permissions only context
    2. A hybrid context with old-style permissions plus an "AlertsConfig"
    3. A future context with a "Permissions" and an "AlertsConfig" as sibling
       properties

When the API is migrated to scenario #3 above, the custom marshaler can be
removed.

Part of BACK-2500
  • Loading branch information
ewollesen committed Sep 8, 2023
1 parent 837e108 commit d5687dc
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 6 deletions.
1 change: 1 addition & 0 deletions api/hydrophoneApi.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
STATUS_ERR_FINDING_USER = "Error finding the user"
STATUS_ERR_FINDING_CLINIC = "Error finding the clinic"
STATUS_ERR_DECODING_CONFIRMATION = "Error decoding the confirmation"
STATUS_ERR_DECODING_CONTEXT = "Error decoding the confirmation context"
STATUS_ERR_CREATING_PATIENT = "Error creating patient"
STATUS_ERR_FINDING_PREVIEW = "Error finding the invite preview"

Expand Down
10 changes: 7 additions & 3 deletions api/invite.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,13 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[
return
}

var permissions commonClients.Permissions
conf.DecodeContext(&permissions)
setPerms, err := a.gatekeeper.SetPermissions(inviteeID, invitorID, permissions)
ctc := &models.CareTeamContext{}
if err := conf.DecodeContext(ctc); err != nil {
statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONTEXT)}
a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest)
return
}
setPerms, err := a.gatekeeper.SetPermissions(inviteeID, invitorID, ctc.Permissions)
if err != nil {
log.Printf("AcceptInvite error setting permissions [%v]\n", err)
a.sendModelAsResWithStatus(
Expand Down
60 changes: 60 additions & 0 deletions models/confirmation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"fmt"
"log"
"time"

"github.com/tidepool-org/go-common/clients"
)

type (
Expand Down Expand Up @@ -249,6 +251,64 @@ func generateKey() (string, error) {
}
}

// CareTeamContext specifies details associated with a Care Team Confirmation.
type CareTeamContext struct {
// Permissions to be granted if the Confirmation is accepted.
Permissions clients.Permissions `json:"permissions"`
// AlertsConfig is the initial configuration of alerts for the invitee.
AlertsConfig *AlertsConfig `json:"alertsConfig,omitempty"`
}

// UnmarshalJSON handles different iterations of Care Team Context.
//
// Originally the context was a go-common clients.Permissions
// (map[string]map[string]interface{}), but with care partner alerting it
// became necessary to handle both the older Permissions only Contexts, but
// also newer Contexts in which Permissions are stored under a key. In
// addition a hybrid Context is supported, where if permissions aren't found
// under a key, it's assumed that every other keys is an individual
// client.Permission.
//
// WARNING: this works only if the newly added context fields don't share a
// name with a previously used permission-type. Right now that means "note",
// "upload", and "view".
//
// If the API is migrated so this custom unmarshaler isn't necessary, that
// would be a good thing.
func (e *CareTeamContext) UnmarshalJSON(b []byte) error {
const permissionsKey string = "permissions"
const alertsConfigKey string = "alertsConfig"

generic := map[string]json.RawMessage{}
if err := json.Unmarshal(b, &generic); err != nil {
return fmt.Errorf("unmarshaling Confirmation Context: %w", err)
}
if genericAlertsConfig := generic[alertsConfigKey]; genericAlertsConfig != nil {
if err := json.Unmarshal(genericAlertsConfig, &e.AlertsConfig); err != nil {
return fmt.Errorf("unmarshaling AlertsConfig: %w", err)
}
delete(generic, alertsConfigKey)
}
if genericPermissions := generic[permissionsKey]; genericPermissions != nil {
if err := json.Unmarshal(genericPermissions, &e.Permissions); err != nil {
return fmt.Errorf("unmarshaling hybrid Permissions: %w", err)
}
delete(generic, permissionsKey)
} else {
e.Permissions = clients.Permissions{}
for k := range generic {
perm := clients.Permission{}
err := json.Unmarshal(generic[k], &perm)
if err != nil {
return fmt.Errorf("unmarshaling Permissions: %w", err)
}
e.Permissions[k] = perm
}
}

return nil
}

// AlertsConfig is included with a care team invitation to configure initial
// alerts for the invitation's recipient.
type AlertsConfig struct {
Expand Down
93 changes: 90 additions & 3 deletions models/confirmation_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package models

import (
"encoding/json"
"testing"
"time"

"github.com/tidepool-org/go-common/clients"
)

const USERID = "1234-555"
Expand Down Expand Up @@ -96,13 +99,18 @@ func Test_NewConfirmationWithContext(t *testing.T) {

func Test_Confirmation_AddContext(t *testing.T) {

confirmation, _ := NewConfirmation(TypePasswordReset, TemplateNamePasswordReset, USERID)

confirmation, err := NewConfirmation(TypePasswordReset, TemplateNamePasswordReset, USERID)
if err != nil {
t.Fatalf("expected nil, got %+v", err)
}
confirmation.AddContext(contextData)

myExtras := &Extras{}

confirmation.DecodeContext(&myExtras)
err = confirmation.DecodeContext(&myExtras)
if err != nil {
t.Fatalf("expected nil, got %+v", err)
}

if myExtras.Blah == "" {
t.Fatalf("context not decoded [%v]", myExtras)
Expand All @@ -127,6 +135,85 @@ func TestConfirmationKey(t *testing.T) {
}
}

func TestConfirmationContextCustomUnmarshaler(s *testing.T) {
s.Run("handles original-recipe Context (aka bare Permissions)", func(t *testing.T) {
perms, err := json.Marshal(clients.Permissions{
"view": clients.Allowed,
})
if err != nil {
t.Fatalf("expected nil, got %+v", err)
}

cc := &CareTeamContext{}
if err := json.Unmarshal(perms, cc); err != nil {
t.Fatalf("expected nil, got %+v", err)
}
if view := cc.Permissions["view"]; view == nil {
t.Fatal("expected view permissions to not be nil, got nil")
}
})

s.Run("handles a hybrid Context with AlertsConfig and old-style permissions", func(t *testing.T) {
hybrid, err := json.Marshal(map[string]interface{}{
"view": clients.Allowed,
"alertsConfig": &AlertsConfig{
Low: AlertConfig{Threshold: 100},
},
})
if err != nil {
t.Fatalf("expected nil, got %+v", err)
}

cc := &CareTeamContext{}
if err := json.Unmarshal(hybrid, cc); err != nil {
t.Fatalf("expected nil, got %+v", err)
}
if view := cc.Permissions["view"]; view == nil {
t.Fatal("expected view permissions to not be nil, got nil")
}
if alerts := cc.AlertsConfig; alerts == nil {
t.Fatal("expected alerts config to not be nil, got nil")
}
if low := cc.AlertsConfig.Low; low.Threshold != 100 {
t.Fatalf("expected 100, got %d", low.Threshold)
}
})

s.Run("handles a Context that strictly matches CareTeamContext", func(t *testing.T) {
context, err := json.Marshal(map[string]interface{}{
"permissions": clients.Permissions{
"view": clients.Allowed,
},
"alertsConfig": &AlertsConfig{
Low: AlertConfig{Threshold: 100},
},
"ignored": clients.Allowed,
})
if err != nil {
t.Fatalf("expected nil, got %s", err)
}

cc := &CareTeamContext{}
if err := json.Unmarshal(context, cc); err != nil {
t.Fatalf("expected nil, got %s", err)
}
if view := cc.Permissions["view"]; view == nil {
t.Fatal("expected view permissions to not be nil, got nil")
}
// Since a "permissions" key is found, any additional keys (like
// "ignored") should be… ignored.
if cc.Permissions["ignored"] != nil {
t.Fatal("expected \"ignored\" to be ignored, but is present")
}
if alerts := cc.AlertsConfig; alerts == nil {
t.Fatal("expected alerts config to not be nil, got nil")
}
if low := cc.AlertsConfig.Low; low.Threshold != 100 {
t.Fatalf("expected 100, got %d", low.Threshold)
}
})
}

func TestDurationMinutes(s *testing.T) {
s.Run("parses 10", func(t *testing.T) {
d := DurationMinutes(0)
Expand Down

0 comments on commit d5687dc

Please sign in to comment.