Add custom drive item (#4783)

<!-- PR description-->

Add a `custom.DriveItem` struct which partially replicates `DriveItemable` and only stores the fields corso makes use of during a backup operation. 

Sorry about the PR size, I don't have a way to split this into multiple PRs. Also it should be trivial to review because it's simply copying over fields to another struct.

---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [x] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Abhishek Pandey 2023-12-05 13:40:11 -08:00 committed by GitHub
parent 02e9e1310e
commit b5c9199695
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 892 additions and 0 deletions

View File

@ -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
}

View File

@ -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)
})
}
}