From d56c6445294bcd453e34632a0737246120f9a4da Mon Sep 17 00:00:00 2001 From: jkoberg Date: Thu, 6 Jun 2024 12:14:20 +0200 Subject: [PATCH] feat(activitylog): unit test activity storing Signed-off-by: jkoberg --- services/activitylog/pkg/service/service.go | 56 +++++- .../activitylog/pkg/service/service_test.go | 181 ++++++++++++++++++ 2 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 services/activitylog/pkg/service/service_test.go diff --git a/services/activitylog/pkg/service/service.go b/services/activitylog/pkg/service/service.go index e278957abb..d3421608a9 100644 --- a/services/activitylog/pkg/service/service.go +++ b/services/activitylog/pkg/service/service.go @@ -70,7 +70,7 @@ func (a *ActivitylogService) Run() error { var err error switch ev := e.Event.(type) { case events.UploadReady: - err = a.addActivity(ev.FileRef, e.ID, utils.TSToTime(ev.Timestamp)) + err = a.AddActivity(ev.FileRef, e.ID, utils.TSToTime(ev.Timestamp)) } if err != nil { @@ -80,7 +80,8 @@ func (a *ActivitylogService) Run() error { return nil } -func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID string, timestamp time.Time) error { +// AddActivity addds the activity to the given resource and all its parents +func (a *ActivitylogService) AddActivity(initRef *provider.Reference, eventID string, timestamp time.Time) error { gwc, err := a.gws.Next() if err != nil { return fmt.Errorf("cant get gateway client: %w", err) @@ -91,22 +92,57 @@ func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID st return fmt.Errorf("cant get service user context: %w", err) } - var info *provider.ResourceInfo - depth, ref := 0, initRef + return a.addActivity(initRef, eventID, timestamp, func(ref *provider.Reference) (*provider.ResourceInfo, error) { + return utils.GetResource(ctx, ref, gwc) + }) +} + +// Activities returns the activities for the given reference +func (a *ActivitylogService) Activities(ref *provider.Reference) ([]Activity, error) { + resourceID, err := storagespace.FormatReference(ref) + if err != nil { + return nil, fmt.Errorf("could not format reference: %w", err) + } + + records, err := a.store.Read(resourceID) + if err != nil && err != microstore.ErrNotFound { + return nil, fmt.Errorf("could not read activities: %w", err) + } + + if len(records) == 0 { + return []Activity{}, nil + } + + var activities []Activity + if err := json.Unmarshal(records[0].Value, &activities); err != nil { + return nil, fmt.Errorf("could not unmarshal activities: %w", err) + } + + return activities, nil +} + +// note: getResource is abstracted to allow unit testing, in general this will just be utils.GetResource +func (a *ActivitylogService) addActivity(initRef *provider.Reference, eventID string, timestamp time.Time, getResource func(*provider.Reference) (*provider.ResourceInfo, error)) error { + var ( + info *provider.ResourceInfo + err error + depth int + ref = initRef + ) for { if err := a.addActivityToReference(ref, eventID, depth, timestamp); err != nil { return fmt.Errorf("could not store activity: %w", err) } - if info != nil && utils.IsSpaceRoot(info) { - return nil - } - - info, err = utils.GetResource(ctx, ref, gwc) + info, err = getResource(ref) if err != nil { return fmt.Errorf("could not get resource info: %w", err) } + if info != nil && utils.IsSpaceRoot(info) { + return nil + } + depth++ ref = &provider.Reference{ResourceId: info.GetParentId()} } @@ -127,7 +163,7 @@ func (a *ActivitylogService) addActivityToReference(ref *provider.Reference, eve func (a *ActivitylogService) storeActivity(resourceID string, activity Activity) error { records, err := a.store.Read(resourceID) - if err != nil { + if err != nil && err != microstore.ErrNotFound { return err } diff --git a/services/activitylog/pkg/service/service_test.go b/services/activitylog/pkg/service/service_test.go new file mode 100644 index 0000000000..616e11c700 --- /dev/null +++ b/services/activitylog/pkg/service/service_test.go @@ -0,0 +1,181 @@ +package service + +import ( + "testing" + "time" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/pkg/store" + "github.com/stretchr/testify/require" +) + +func TestAddActivity(t *testing.T) { + testCases := []struct { + Name string + Tree map[string]*provider.ResourceInfo + Activities map[string]string + Expected map[string][]Activity + }{ + { + Name: "simple", + Tree: map[string]*provider.ResourceInfo{ + "base": resourceInfo("base", "parent"), + "parent": resourceInfo("parent", "spaceid"), + "spaceid": resourceInfo("spaceid", "spaceid"), + }, + Activities: map[string]string{ + "activity": "base", + }, + Expected: map[string][]Activity{ + "base": activitites("activity", 0), + "parent": activitites("activity", 1), + "spaceid": activitites("activity", 2), + }, + }, + { + Name: "two activities on same resource", + Tree: map[string]*provider.ResourceInfo{ + "base": resourceInfo("base", "parent"), + "parent": resourceInfo("parent", "spaceid"), + "spaceid": resourceInfo("spaceid", "spaceid"), + }, + Activities: map[string]string{ + "activity1": "base", + "activity2": "base", + }, + Expected: map[string][]Activity{ + "base": activitites("activity1", 0, "activity2", 0), + "parent": activitites("activity1", 1, "activity2", 1), + "spaceid": activitites("activity1", 2, "activity2", 2), + }, + }, + { + Name: "two activities on different resources", + Tree: map[string]*provider.ResourceInfo{ + "base1": resourceInfo("base1", "parent"), + "base2": resourceInfo("base2", "parent"), + "parent": resourceInfo("parent", "spaceid"), + "spaceid": resourceInfo("spaceid", "spaceid"), + }, + Activities: map[string]string{ + "activity1": "base1", + "activity2": "base2", + }, + Expected: map[string][]Activity{ + "base1": activitites("activity1", 0), + "base2": activitites("activity2", 0), + "parent": activitites("activity1", 1, "activity2", 1), + "spaceid": activitites("activity1", 2, "activity2", 2), + }, + }, + { + Name: "more elaborate resource tree", + Tree: map[string]*provider.ResourceInfo{ + "base1": resourceInfo("base1", "parent1"), + "base2": resourceInfo("base2", "parent1"), + "parent1": resourceInfo("parent1", "spaceid"), + "base3": resourceInfo("base3", "parent2"), + "parent2": resourceInfo("parent2", "spaceid"), + "spaceid": resourceInfo("spaceid", "spaceid"), + }, + Activities: map[string]string{ + "activity1": "base1", + "activity2": "base2", + "activity3": "base3", + }, + Expected: map[string][]Activity{ + "base1": activitites("activity1", 0), + "base2": activitites("activity2", 0), + "base3": activitites("activity3", 0), + "parent1": activitites("activity1", 1, "activity2", 1), + "parent2": activitites("activity3", 1), + "spaceid": activitites("activity1", 2, "activity2", 2, "activity3", 2), + }, + }, + { + Name: "different depths within one resource", + Tree: map[string]*provider.ResourceInfo{ + "base1": resourceInfo("base1", "parent1"), + "parent1": resourceInfo("parent1", "parent2"), + "base2": resourceInfo("base2", "parent2"), + "parent2": resourceInfo("parent2", "parent3"), + "base3": resourceInfo("base3", "parent3"), + "parent3": resourceInfo("parent3", "spaceid"), + "spaceid": resourceInfo("spaceid", "spaceid"), + }, + Activities: map[string]string{ + "activity1": "base1", + "activity2": "base2", + "activity3": "base3", + "activity4": "parent2", + }, + Expected: map[string][]Activity{ + "base1": activitites("activity1", 0), + "base2": activitites("activity2", 0), + "base3": activitites("activity3", 0), + "parent1": activitites("activity1", 1), + "parent2": activitites("activity1", 2, "activity2", 1, "activity4", 0), + "parent3": activitites("activity1", 3, "activity2", 2, "activity3", 1, "activity4", 1), + "spaceid": activitites("activity1", 4, "activity2", 3, "activity3", 2, "activity4", 2), + }, + }, + } + + for _, tc := range testCases { + alog := &ActivitylogService{ + store: store.Create(), + } + + getResource := func(ref *provider.Reference) (*provider.ResourceInfo, error) { + return tc.Tree[ref.GetResourceId().GetOpaqueId()], nil + } + + for k, v := range tc.Activities { + err := alog.addActivity(reference(v), k, time.Time{}, getResource) + require.NoError(t, err) + } + + for id, acts := range tc.Expected { + activities, err := alog.Activities(reference(id)) + require.NoError(t, err, tc.Name+":"+id) + require.ElementsMatch(t, acts, activities, tc.Name+":"+id) + } + } +} + +func activitites(acts ...interface{}) []Activity { + var activities []Activity + act := Activity{} + for _, a := range acts { + switch v := a.(type) { + case string: + act.EventID = v + case int: + act.Depth = v + activities = append(activities, act) + } + } + return activities +} + +func resourceID(id string) *provider.ResourceId { + return &provider.ResourceId{ + StorageId: "storageid", + OpaqueId: id, + SpaceId: "spaceid", + } +} + +func reference(id string) *provider.Reference { + return &provider.Reference{ResourceId: resourceID(id)} +} + +func resourceInfo(id, parentID string) *provider.ResourceInfo { + return &provider.ResourceInfo{ + Id: resourceID(id), + ParentId: resourceID(parentID), + Space: &provider.StorageSpace{ + Root: resourceID("spaceid"), + }, + } +}