diff --git a/bindings/notifications.go b/bindings/notifications.go index 1bd400882d3ddf9981752d0937fd2e358071258f..bd7c75506b81945f0b0c3e293cd1f6b750ea1460 100644 --- a/bindings/notifications.go +++ b/bindings/notifications.go @@ -29,7 +29,23 @@ type Notifications interface { AddToken(newToken, app string) error // RemoveToken removes the given Token from the server // It will remove all registered identities if it is the last Token + RemoveToken() error + // SetMaxState sets the maximum functional state of any identity + // downstream moduals will be told to clamp any state greater than maxState + // down to maxState. Depending on UX requirements, they may still show the + // state in an altered manner, for example greying out a description. + // This is designed so when the state is raised, the old configs are + // maintained. + // This will unregister / re-register with the push server when leaving or + // entering the Push maxState. + // The default maxState is Push + // will return an error if the maxState isnt a valid state + SetMaxState(maxState notifications.NotificationState) error + + // GetMaxState returns the current MaxState + GetMaxState() notifications.NotificationState + // GetID returns the ID of the notifications object GetID() int } diff --git a/bindings/notifications_test.go b/bindings/notifications_test.go deleted file mode 100644 index 66b6f63e558f4973eea60b2df6ff2784cac79391..0000000000000000000000000000000000000000 --- a/bindings/notifications_test.go +++ /dev/null @@ -1,36 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package bindings - -import ( - "encoding/json" - "fmt" - "testing" - - "gitlab.com/elixxir/client/v4/e2e/ratchet" - "gitlab.com/xx_network/primitives/id" -) - -func TestNotificationReport(t *testing.T) { - var reports []NotificationReport - - for i := 0; i < 3; i++ { - nr := NotificationReport{ - ForMe: true, - Type: []string{ratchet.E2e}, - Source: id.NewIdFromUInt(uint64(i), id.User, t).Bytes(), - } - - reports = append(reports, nr) - } - - nrs := NotificationReports(reports) - - marshal, _ := json.MarshalIndent(nrs, " ", " ") - fmt.Printf("%s\n", marshal) -} diff --git a/notifications/interface_test.go b/notifications/interface_test.go index 948fa4e27bb5972673604243b3a69c1f5ab9e2c5..ebdcdd687759e9be0fd70017796feb7ce1e15b54 100644 --- a/notifications/interface_test.go +++ b/notifications/interface_test.go @@ -2,6 +2,7 @@ package notifications import ( "gitlab.com/xx_network/primitives/id" + "math" "testing" ) @@ -61,3 +62,18 @@ func TestNotificationState_String(t *testing.T) { } } } + +func TestNotificationState_IsValid(t *testing.T) { + inputs := []NotificationState{Mute, WhenOpen, Push, -100, 14, Mute - 1, + Push + 1, 1000, math.MaxInt64, math.MinInt64} + outputs := []bool{true, true, true, false, false, false, false, false, + false, false} + + for idx, ns := range inputs { + result := ns.IsValid() == nil + if outputs[idx] != result { + t.Errorf("on test %d output did not match expected, "+ + "expected: %t, received: %t", idx, outputs[idx], result) + } + } +} diff --git a/notifications/manager.go b/notifications/manager.go index cd4f262de8af346a11b23f5385fdc8e31bbb6b7a..070e9de56e7aeaa2d9315090045bb456596d8507 100644 --- a/notifications/manager.go +++ b/notifications/manager.go @@ -230,6 +230,7 @@ func (m *manager) maxStateUpdate(key string, old, new *versioned.Object, op vers if key != maxStateKey { jww.ERROR.Printf("Got an update for the wrong key, "+ "expected: %s, got: %s", maxStateKey, key) + return } if op == versioned.Deleted { @@ -242,6 +243,7 @@ func (m *manager) maxStateUpdate(key string, old, new *versioned.Object, op vers if err := json.Unmarshal(new.Data, &m.maxState); err != nil { jww.WARN.Printf("failed to unmarshal %s, ignoring: %+v", maxStateKey, err) + return } for g := range m.callbacks { @@ -260,7 +262,7 @@ func (m *manager) loadMaxStateUnsafe(obj *versioned.Object) { func (m *manager) setMaxStateUnsafe(max NotificationState) { - b, err := json.Marshal(&m.maxState) + b, err := json.Marshal(&max) if err != nil { jww.FATAL.Panicf("Failed to set max notifications sate to %s:"+ " %+v", max, err) diff --git a/notifications/manager_test.go b/notifications/manager_test.go index e0dfdd5d14a196f652c6fcb84e474a57ad791a5c..1dab4e4d187970407a8cccc0d1296375b18a1890 100644 --- a/notifications/manager_test.go +++ b/notifications/manager_test.go @@ -397,6 +397,123 @@ func getGroup(i, numGroups int) int { return i % numGroups } +func TestManager_maxStateUpdate(t *testing.T) { + m, _, _ := buildTestingManager(t) + mInternal := m.(*manager) + + mInternal.maxStateUpdate("blah", nil, nil, versioned.Deleted) + // key check worked becasue we didnt crash from the delete panic + + numGroups := 4 + numCB := numGroups / 2 + wg := &sync.WaitGroup{} + + didRun := make([]bool, numGroups) + expectedRun := []bool{true, false, true, false} + + var setMax NotificationState + + for i := 0; i < numGroups; i++ { + groupName := fmt.Sprintf("grp_%d", i) + nid := id.NewIdFromUInt(uint64(i), id.User, t) + m.Set(nid, groupName, []byte{0}, NotificationState(i%3)) + if i%2 == 0 { + localI := i + cb := func(group Group, created, edits, deletions []*id.ID, + maxState NotificationState) { + if created != nil || edits != nil || deletions != nil { + t.Errorf("actions are not nil") + } + if maxState != setMax { + t.Errorf("max state incorrect, expected %s, "+ + "received %s", setMax, maxState) + } + didRun[localI] = true + wg.Done() + } + m.RegisterUpdateCallback(groupName, cb) + } + } + + for i := Mute; i <= Push; i++ { + setMax = i + for j := versioned.Created; j <= versioned.Deleted; j++ { + ch := make(chan bool) + didRun = make([]bool, numGroups) + if j != versioned.Deleted { + wg.Add(numCB) + } + + go func() { + defer func() { + if r := recover(); r != nil { + ch <- false + } + }() + mInternal.maxStateUpdate(maxStateKey, nil, + makeMaxStateObj(i, t), j) + ch <- true + }() + result := <-ch + if j == versioned.Deleted && result { + t.Errorf("did not panic on deletion") + } else if j != versioned.Deleted && !result { + t.Errorf("panicked on non deletion") + } + if j != versioned.Deleted { + wg.Wait() + if mInternal.maxState != i { + t.Errorf("max state not set to %s, is %s", i, mInternal.maxState) + } + if !reflect.DeepEqual(expectedRun, didRun) { + t.Errorf("wrong callbacks were called, %+v", didRun) + } + } + + } + } + +} + +func makeMaxStateObj(max NotificationState, t *testing.T) *versioned.Object { + b, err := json.Marshal(&max) + if err != nil { + t.Fatalf("Failed to marshal max state: %+v", err) + } + return &versioned.Object{ + Version: maxStateKetVersion, + Timestamp: time.Now(), + Data: b, + } +} + +func TestManager_setLoadMaxState(t *testing.T) { + m, _, _ := buildTestingManager(t) + mInternal := m.(*manager) + + mInternal.maxState = -1 + + tests := []NotificationState{Mute, WhenOpen, Push} + + for _, ns := range tests { + mInternal.maxState = -1 + mInternal.setMaxStateUnsafe(ns) + if mInternal.maxState != ns { + t.Errorf("In ram max state did not take with set") + } + mInternal.maxState = -1 + obj, err := mInternal.remote.Get(maxStateKey, maxStateKetVersion) + if err != nil { + t.Errorf("Failed to get max state %s from ekv: %+v", ns, err) + } + mInternal.loadMaxStateUnsafe(obj) + if mInternal.maxState != ns { + t.Errorf("In ram max state did not take with load of %s", + string(obj.Data)) + } + } +} + func TestManager_upsertNotificationUnsafeRAM(t *testing.T) { m, _, _ := buildTestingManager(t) mInternal := m.(*manager) @@ -645,6 +762,7 @@ type commsMock struct { receivedMessage interface{} returnedMessage *messages.Ack returnedError error + numRuns int } func initCommsMock() *commsMock { @@ -664,6 +782,7 @@ func (cm *commsMock) RegisterToken(host *connect.Host, message *pb.RegisterTokenRequest) (*messages.Ack, error) { cm.receivedHost = host cm.receivedMessage = message + cm.numRuns++ return cm.returnedMessage, cm.returnedError } @@ -671,6 +790,7 @@ func (cm *commsMock) UnregisterToken(host *connect.Host, message *pb.UnregisterTokenRequest) (*messages.Ack, error) { cm.receivedHost = host cm.receivedMessage = message + cm.numRuns++ return cm.returnedMessage, cm.returnedError } @@ -678,6 +798,7 @@ func (cm *commsMock) RegisterTrackedID(host *connect.Host, message *pb.RegisterTrackedIdRequest) (*messages.Ack, error) { cm.receivedHost = host cm.receivedMessage = message + cm.numRuns++ return cm.returnedMessage, cm.returnedError } @@ -685,6 +806,7 @@ func (cm *commsMock) UnregisterTrackedID(host *connect.Host, message *pb.UnregisterTrackedIdRequest) (*messages.Ack, error) { cm.receivedHost = host cm.receivedMessage = message + cm.numRuns++ return cm.returnedMessage, cm.returnedError } diff --git a/notifications/maxState_test.go b/notifications/maxState_test.go new file mode 100644 index 0000000000000000000000000000000000000000..25e8e8a56bdcc5031ec650419d8fd0ccb6e45e23 --- /dev/null +++ b/notifications/maxState_test.go @@ -0,0 +1,101 @@ +package notifications + +import ( + "encoding/json" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/xx_network/primitives/id" + "testing" +) + +func TestManager_SetMaxState(t *testing.T) { + m, _, comms := buildTestingManager(t) + mInternal := m.(*manager) + + expectedLen := int(Push) + 1 + + comms.reset() + + for i := Mute; i <= Push; i++ { + for x := 0; x < int(i)+1; x++ { + nid := id.NewIdFromUInt(uint64(int(i)*100+x), id.User, t) + if err := m.Set(nid, "a", []byte{0}, i); err != nil { + t.Errorf("errored in set: %+v", err) + } + } + } + + if err := m.SetMaxState(Mute); err != nil { + t.Fatalf("errored in setMaxState: %+v", err) + } + + unReg := comms.receivedMessage.(*pb.UnregisterTrackedIdRequest) + if len(unReg.Request.TrackedIntermediaryID) != expectedLen { + t.Errorf("wrong number of ids unregistered") + } + + if mInternal.maxState != Mute { + t.Errorf("max state at wrong state internally") + } + + if loadMaxState(mInternal, t) != Mute { + t.Errorf("max state at wrong state in ekv") + } + + comms.reset() + if err := m.SetMaxState(WhenOpen); err != nil { + t.Fatalf("errored in setMaxState: %+v", err) + } + if comms.receivedMessage != nil { + t.Errorf("message sent when it shouldnt be!") + } + if mInternal.maxState != WhenOpen { + t.Errorf("max state at wrong state internally") + } + + if loadMaxState(mInternal, t) != WhenOpen { + t.Errorf("max state at wrong state in ekv") + } + comms.reset() + if err := m.SetMaxState(Push); err != nil { + t.Fatalf("errored in setMaxState: %+v", err) + } + + reg := comms.receivedMessage.(*pb.RegisterTrackedIdRequest) + if len(reg.Request.TrackedIntermediaryID) != expectedLen { + t.Errorf("wrong number of ids unregistered") + } + + if mInternal.maxState != Push { + t.Errorf("max state at wrong state internally") + } + + if loadMaxState(mInternal, t) != Push { + t.Errorf("max state at wrong state in ekv") + } +} + +func TestManager_GetMaxState(t *testing.T) { + m, _, _ := buildTestingManager(t) + mInternal := m.(*manager) + + for i := Mute; i <= Push; i++ { + mInternal.maxState = i + got := m.GetMaxState() + if i != got { + t.Errorf("get didnt match value") + } + } +} + +func loadMaxState(m *manager, t *testing.T) NotificationState { + obj, err := m.remote.Get(maxStateKey, maxStateKetVersion) + if err != nil { + t.Fatalf("could not get max state: %+v", err) + } + var ms NotificationState + err = json.Unmarshal(obj.Data, &ms) + if err != nil { + t.Fatalf("could not get max state: %+v", err) + } + return ms +}