diff --git a/src/pkg/services/m365/custom/drive_item.go b/src/pkg/services/m365/custom/drive_item.go new file mode 100644 index 000000000..75fa3c911 --- /dev/null +++ b/src/pkg/services/m365/custom/drive_item.go @@ -0,0 +1,363 @@ +package custom + +import ( + "strings" + "time" + + "github.com/microsoftgraph/msgraph-sdk-go/models" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/common/str" +) + +// --------------------------------------------------------------------------- +// DriveItem +// --------------------------------------------------------------------------- +type DriveItem struct { + id *string + name *string + size *int64 + createdDateTime *time.Time + lastModifiedDateTime *time.Time + folder *struct{} + pkg *struct{} + shared *struct{} + deleted *struct{} + root *struct{} + malware *malware + file *fileItem + parentRef *itemReference + createdBy *identitySet + createdByUser *user + lastModifiedByUser *user + additionalData map[string]any +} + +// Disable revive linter since we want to follow naming scheme used by graph SDK here. +// nolint: revive +func (c *DriveItem) GetId() *string { + return c.id +} + +func (c *DriveItem) GetName() *string { + return c.name +} + +func (c *DriveItem) GetSize() *int64 { + return c.size +} + +func (c *DriveItem) GetCreatedDateTime() *time.Time { + return c.createdDateTime +} + +func (c *DriveItem) GetLastModifiedDateTime() *time.Time { + return c.lastModifiedDateTime +} + +func (c *DriveItem) GetFolder() *struct{} { + return c.folder +} + +func (c *DriveItem) GetPackageEscaped() *struct{} { + return c.pkg +} + +func (c *DriveItem) GetShared() *struct{} { + return c.shared +} + +func (c *DriveItem) GetDeleted() *struct{} { + return c.deleted +} + +func (c *DriveItem) GetRoot() *struct{} { + return c.root +} + +func (c *DriveItem) GetMalware() *malware { + return c.malware +} + +func (c *DriveItem) GetFile() *fileItem { + return c.file +} + +func (c *DriveItem) GetParentReference() *itemReference { + return c.parentRef +} + +func (c *DriveItem) SetParentReference(parent *itemReference) { + c.parentRef = parent +} + +func (c *DriveItem) GetCreatedBy() *identitySet { + return c.createdBy +} + +func (c *DriveItem) GetCreatedByUser() *user { + return c.createdByUser +} + +func (c *DriveItem) GetLastModifiedByUser() *user { + return c.lastModifiedByUser +} + +func (c *DriveItem) GetAdditionalData() map[string]any { + return c.additionalData +} + +// --------------------------------------------------------------------------- +// malware +// --------------------------------------------------------------------------- +type malware struct { + description *string +} + +func (m *malware) GetDescription() *string { + return m.description +} + +// --------------------------------------------------------------------------- +// fileItem +// --------------------------------------------------------------------------- +type fileItem struct { + mimeType *string +} + +func (f *fileItem) GetMimeType() *string { + return f.mimeType +} + +// --------------------------------------------------------------------------- +// itemReference +// --------------------------------------------------------------------------- +type itemReference struct { + path *string + id *string + name *string + driveID *string +} + +func (ir *itemReference) GetPath() *string { + return ir.path +} + +// nolint: revive +func (ir *itemReference) GetId() *string { + return ir.id +} + +func (ir *itemReference) GetName() *string { + return ir.name +} + +// nolint: revive +func (ir *itemReference) GetDriveId() *string { + return ir.driveID +} + +// --------------------------------------------------------------------------- +// identitySet +// --------------------------------------------------------------------------- +type identitySet struct { + identity *identity +} + +func (iis *identitySet) GetUser() *identity { + return iis.identity +} + +// --------------------------------------------------------------------------- +// identity +// --------------------------------------------------------------------------- +type identity struct { + additionalData map[string]any +} + +func (i *identity) GetAdditionalData() map[string]any { + return i.additionalData +} + +// --------------------------------------------------------------------------- +// user +// --------------------------------------------------------------------------- +type user struct { + id *string +} + +// nolint: revive +func (u *user) GetId() *string { + return u.id +} + +// TODO(pandeyabs): This is duplicated from collection/drive package. +// Move to api/graph. +var downloadURLKeys = []string{ + "@microsoft.graph.downloadUrl", + "@content.downloadUrl", +} + +// ToCustomDriveItem converts a DriveItemable to a flattened DriveItem struct +// that stores only the properties we care about during the backup operation. +func ToCustomDriveItem(item models.DriveItemable) *DriveItem { + if item == nil { + return nil + } + + di := &DriveItem{} + + if item.GetId() != nil { + itemID := strings.Clone(ptr.Val(item.GetId())) + di.id = &itemID + } + + if item.GetName() != nil { + itemName := strings.Clone(ptr.Val(item.GetName())) + di.name = &itemName + } + + if item.GetSize() != nil { + itemSize := ptr.Val(item.GetSize()) + di.size = &itemSize + } + + if item.GetCreatedDateTime() != nil { + createdTime := ptr.Val(item.GetCreatedDateTime()) + di.createdDateTime = &createdTime + } + + if item.GetLastModifiedDateTime() != nil { + lastModifiedTime := ptr.Val(item.GetLastModifiedDateTime()) + di.lastModifiedDateTime = &lastModifiedTime + } + + if item.GetFolder() != nil { + di.folder = &struct{}{} + } + + if item.GetPackageEscaped() != nil { + di.pkg = &struct{}{} + } + + if item.GetMalware() != nil { + mw := &malware{} + + if item.GetMalware().GetDescription() != nil { + desc := strings.Clone(ptr.Val(item.GetMalware().GetDescription())) + mw.description = &desc + } + + di.malware = mw + } + + if item.GetFile() != nil { + fi := &fileItem{} + + if item.GetFile().GetMimeType() != nil { + mimeType := strings.Clone(ptr.Val(item.GetFile().GetMimeType())) + fi.mimeType = &mimeType + } + + di.file = fi + } + + if item.GetParentReference() != nil { + iRef := &itemReference{} + + if item.GetParentReference().GetId() != nil { + parentID := strings.Clone(ptr.Val(item.GetParentReference().GetId())) + iRef.id = &parentID + } + + if item.GetParentReference().GetPath() != nil { + parentPath := strings.Clone(ptr.Val(item.GetParentReference().GetPath())) + iRef.path = &parentPath + } + + if item.GetParentReference().GetName() != nil { + parentName := strings.Clone(ptr.Val(item.GetParentReference().GetName())) + iRef.name = &parentName + } + + if item.GetParentReference().GetDriveId() != nil { + parentDriveID := strings.Clone(ptr.Val(item.GetParentReference().GetDriveId())) + iRef.driveID = &parentDriveID + } + + di.parentRef = iRef + } + + if item.GetShared() != nil { + di.shared = &struct{}{} + } + + if item.GetDeleted() != nil { + di.deleted = &struct{}{} + } + + if item.GetRoot() != nil { + di.root = &struct{}{} + } + + if item.GetCreatedBy() != nil { + createdBy := &identitySet{} + + if item.GetCreatedBy().GetUser() != nil { + additionalData := item.GetCreatedBy().GetUser().GetAdditionalData() + ad := make(map[string]any) + + if v, err := str.AnyValueToString("email", additionalData); err == nil { + email := strings.Clone(v) + ad["email"] = &email + } + + if v, err := str.AnyValueToString("displayName", additionalData); err == nil { + displayName := strings.Clone(v) + ad["displayName"] = &displayName + } + + createdBy.identity = &identity{ + additionalData: ad, + } + } + + di.createdBy = createdBy + } + + if item.GetCreatedByUser() != nil { + createdByUser := &user{} + + if item.GetCreatedByUser().GetId() != nil { + userID := strings.Clone(ptr.Val(item.GetCreatedByUser().GetId())) + createdByUser.id = &userID + } + + di.createdByUser = createdByUser + } + + if item.GetLastModifiedByUser() != nil { + lastModifiedByUser := &user{} + + if item.GetLastModifiedByUser().GetId() != nil { + userID := strings.Clone(ptr.Val(item.GetLastModifiedByUser().GetId())) + lastModifiedByUser.id = &userID + } + + di.lastModifiedByUser = lastModifiedByUser + } + + // We only use the download URL from additional data + aData := make(map[string]any) + + for _, key := range downloadURLKeys { + if v, err := str.AnyValueToString(key, item.GetAdditionalData()); err == nil { + downloadURL := strings.Clone(v) + aData[key] = &downloadURL + } + } + + di.additionalData = aData + + return di +} diff --git a/src/pkg/services/m365/custom/drive_item_test.go b/src/pkg/services/m365/custom/drive_item_test.go new file mode 100644 index 000000000..ffd0612d1 --- /dev/null +++ b/src/pkg/services/m365/custom/drive_item_test.go @@ -0,0 +1,529 @@ +// Disable revive linter since any structs in this file will expose the same +// funcs as the original structs in the msgraph-sdk-go package, which do not +// follow some of the golint rules. +// +//nolint:revive +package custom + +import ( + "testing" + "time" + + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gotest.tools/v3/assert" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/common/str" + "github.com/alcionai/corso/src/internal/tester" +) + +type driveItemUnitSuite struct { + tester.Suite +} + +func TestDriveItemUnitSuite(t *testing.T) { + suite.Run(t, &driveItemUnitSuite{ + Suite: tester.NewUnitSuite(t), + }) +} + +func (suite *driveItemUnitSuite) TestToLiteDriveItemable() { + id := "itemID" + + table := []struct { + name string + itemFunc func() models.DriveItemable + validateFunc func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem) + }{ + { + name: "nil item", + itemFunc: func() models.DriveItemable { + return nil + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.Nil(t, got) + }, + }, + { + name: "uninitialized values", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + assert.Equal(t, got.GetId(), (*string)(nil)) + assert.Equal(t, got.GetName(), (*string)(nil)) + assert.Equal(t, got.GetSize(), (*int64)(nil)) + assert.Equal(t, got.GetCreatedDateTime(), (*time.Time)(nil)) + assert.Equal(t, got.GetLastModifiedDateTime(), (*time.Time)(nil)) + require.Nil(t, got.GetFolder()) + require.Nil(t, got.GetFile()) + require.Nil(t, got.GetPackageEscaped()) + require.Nil(t, got.GetShared()) + require.Nil(t, got.GetMalware()) + require.Nil(t, got.GetDeleted()) + require.Nil(t, got.GetRoot()) + require.Nil(t, got.GetCreatedBy()) + require.Nil(t, got.GetCreatedByUser()) + require.Nil(t, got.GetLastModifiedByUser()) + require.Nil(t, got.GetParentReference()) + assert.Equal(t, len(got.GetAdditionalData()), 0) + }, + }, + { + name: "ID, name, size, created, modified", + itemFunc: func() models.DriveItemable { + name := "itemName" + size := int64(6) + created := time.Now().Add(-time.Second) + modified := time.Now() + + di := models.NewDriveItem() + + di.SetId(&id) + di.SetName(&name) + di.SetSize(&size) + di.SetCreatedDateTime(&created) + di.SetLastModifiedDateTime(&modified) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + assert.Equal(t, ptr.Val(got.GetName()), ptr.Val(expected.GetName())) + assert.Equal(t, ptr.Val(got.GetSize()), ptr.Val(expected.GetSize())) + require.True( + t, + got.GetCreatedDateTime().Equal(ptr.Val(expected.GetCreatedDateTime()))) + require.True( + t, + got.GetLastModifiedDateTime().Equal(ptr.Val(expected.GetLastModifiedDateTime()))) + }, + }, + { + name: "Folder item", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + di.SetId(&id) + di.SetFolder(models.NewFolder()) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetFolder()) + require.Nil(t, got.GetFile()) + require.Nil(t, got.GetPackageEscaped()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + { + name: "Package item", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + di.SetId(&id) + di.SetPackageEscaped(models.NewPackageEscaped()) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetPackageEscaped()) + require.Nil(t, got.GetFile()) + require.Nil(t, got.GetFolder()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + // Unlikely but possible that an item is both a folder and a package. + { + name: "Folder as well as Package", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + di.SetId(&id) + di.SetPackageEscaped(models.NewPackageEscaped()) + di.SetFolder(models.NewFolder()) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetPackageEscaped()) + require.NotNil(t, got.GetFolder()) + require.Nil(t, got.GetFile()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + { + name: "File item", + itemFunc: func() models.DriveItemable { + mime := "mimeType" + di := models.NewDriveItem() + + di.SetId(&id) + di.SetFile(models.NewFile()) + di.GetFile().SetMimeType(&mime) + + // Intentionally set different URLs for the two keys to test + // for correctness. It's unlikely that a) both will be set, + // b) URLs will be different, but it's not the responsibility + // of the function being tested here, which is simply copying over + // kv pairs useful to callers. + di.SetAdditionalData(map[string]interface{}{ + "@microsoft.graph.downloadUrl": "downloadURL", + "@content.downloadUrl": "contentURL", + }) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetFile()) + require.Nil(t, got.GetFolder()) + require.Nil(t, got.GetPackageEscaped()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + assert.Equal( + t, + ptr.Val(got.GetFile().GetMimeType()), + ptr.Val(expected.GetFile().GetMimeType())) + + // additional data + urlExpected, err := str.AnyValueToString( + "@microsoft.graph.downloadUrl", + expected.GetAdditionalData()) + require.NoError(t, err) + + urlGot, err := str.AnyValueToString( + "@microsoft.graph.downloadUrl", + got.GetAdditionalData()) + require.NoError(t, err) + + assert.Equal( + t, + urlGot, + urlExpected) + + contentURLExpected, err := str.AnyValueToString( + "@content.downloadUrl", + expected.GetAdditionalData()) + require.NoError(t, err) + + contentURLGot, err := str.AnyValueToString( + "@content.downloadUrl", + got.GetAdditionalData()) + require.NoError(t, err) + + assert.Equal( + t, + contentURLGot, + contentURLExpected) + }, + }, + { + name: "Shared item", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + di.SetId(&id) + di.SetShared(models.NewShared()) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetShared()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + { + name: "Malware item", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + mw := models.NewMalware() + desc := "malware description" + mw.SetDescription(&desc) + + di.SetId(&id) + di.SetMalware(mw) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetMalware()) + assert.Equal( + t, + ptr.Val(expected.GetMalware().GetDescription()), + ptr.Val(got.GetMalware().GetDescription())) + + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + { + name: "Deleted item", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + di.SetId(&id) + di.SetDeleted(models.NewDeleted()) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetDeleted()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + { + name: "Root item", + itemFunc: func() models.DriveItemable { + di := models.NewDriveItem() + + di.SetId(&id) + di.SetRoot(models.NewRoot()) + di.SetFolder(models.NewFolder()) + + return di + }, + + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetRoot()) + require.NotNil(t, got.GetFolder()) + assert.Equal(t, ptr.Val(got.GetId()), ptr.Val(expected.GetId())) + }, + }, + { + name: "Get parent reference", + itemFunc: func() models.DriveItemable { + parentID := "parentID" + parentPath := "/parentPath" + parentName := "parentName" + parentDriveID := "parentDriveID" + + parentRef := models.NewItemReference() + parentRef.SetId(&parentID) + parentRef.SetPath(&parentPath) + parentRef.SetName(&parentName) + parentRef.SetDriveId(&parentDriveID) + + di := models.NewDriveItem() + + di.SetId(&id) + di.SetParentReference(parentRef) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetParentReference()) + assert.Equal( + t, + ptr.Val(got.GetParentReference().GetId()), + ptr.Val(expected.GetParentReference().GetId())) + assert.Equal( + t, + ptr.Val(got.GetParentReference().GetPath()), + ptr.Val(expected.GetParentReference().GetPath())) + assert.Equal( + t, + ptr.Val(got.GetParentReference().GetName()), + ptr.Val(expected.GetParentReference().GetName())) + assert.Equal( + t, + ptr.Val(got.GetParentReference().GetDriveId()), + ptr.Val(expected.GetParentReference().GetDriveId())) + }, + }, + { + name: "Get parent reference with nil fields", + itemFunc: func() models.DriveItemable { + parentRef := models.NewItemReference() + + di := models.NewDriveItem() + + di.SetId(&id) + di.SetParentReference(parentRef) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetParentReference()) + require.Nil(t, got.GetParentReference().GetId()) + require.Nil(t, got.GetParentReference().GetPath()) + require.Nil(t, got.GetParentReference().GetName()) + require.Nil(t, got.GetParentReference().GetDriveId()) + }, + }, + { + name: "Created by", + itemFunc: func() models.DriveItemable { + email := "email@user" + displayName := "username" + + createdBy := models.NewIdentitySet() + + createdBy.SetUser(models.NewUser()) + createdBy.GetUser().SetAdditionalData(map[string]interface{}{ + "email": &email, + "displayName": &displayName, + }) + + di := models.NewDriveItem() + + di.SetId(&id) + di.SetCreatedBy(createdBy) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetCreatedBy()) + require.NotNil(t, got.GetCreatedBy().GetUser()) + emailExpected, err := str.AnyValueToString( + "email", + expected.GetCreatedBy().GetUser().GetAdditionalData()) + require.NoError(t, err) + + emailGot, err := str.AnyValueToString( + "email", + got.GetCreatedBy().GetUser().GetAdditionalData()) + require.NoError(t, err) + + assert.Equal(t, emailGot, emailExpected) + + displayNameExpected, err := str.AnyValueToString( + "displayName", + expected.GetCreatedBy().GetUser().GetAdditionalData()) + require.NoError(t, err) + + displayNameGot, err := str.AnyValueToString( + "displayName", + got.GetCreatedBy().GetUser().GetAdditionalData()) + require.NoError(t, err) + + assert.Equal(t, displayNameGot, displayNameExpected) + }, + }, + { + name: "Created by with nil fields", + itemFunc: func() models.DriveItemable { + createdBy := models.NewIdentitySet() + di := models.NewDriveItem() + + di.SetId(&id) + di.SetCreatedBy(createdBy) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetCreatedBy()) + require.Nil(t, got.GetCreatedBy().GetUser()) + }, + }, + { + name: "Created & last modified by users", + itemFunc: func() models.DriveItemable { + createdByUser := models.NewUser() + uid := "creatorUserID" + createdByUser.SetId(&uid) + + lastModifiedByUser := models.NewUser() + luid := "lastModifierUserID" + lastModifiedByUser.SetId(&luid) + + di := models.NewDriveItem() + + di.SetId(&id) + di.SetCreatedByUser(createdByUser) + di.SetLastModifiedByUser(lastModifiedByUser) + + return di + }, + validateFunc: func( + t *testing.T, + expected models.DriveItemable, + got *DriveItem, + ) { + require.NotNil(t, got.GetCreatedByUser()) + require.NotNil(t, got.GetLastModifiedByUser()) + assert.Equal( + t, + ptr.Val(got.GetCreatedByUser().GetId()), + ptr.Val(expected.GetCreatedByUser().GetId())) + assert.Equal( + t, + ptr.Val(got.GetLastModifiedByUser().GetId()), + ptr.Val(expected.GetLastModifiedByUser().GetId())) + }, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + expected := test.itemFunc() + got := ToCustomDriveItem(expected) + test.validateFunc(suite.T(), expected, got) + }) + } +}