Add Corso extensions package (#3729)

<!-- PR description-->

Introduces interfaces for corso extensions, which is basically wrappers around `io.ReadCloser`.  Will add integration in follow up PRs.

---

#### 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-07-03 12:51:08 -07:00 committed by GitHub
parent cf495a6c8f
commit 85652bfd67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 324 additions and 0 deletions

View File

@ -1044,3 +1044,8 @@ func updateFolderWithinDrive(
return nil return nil
} }
// ExtensionInfo describes extension data associated with an item
type ExtensionInfo struct {
Data map[string]any `json:"data,omitempty"`
}

View File

@ -0,0 +1,61 @@
package extensions
import (
"context"
"io"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/logger"
)
type CreateItemExtensioner interface {
CreateItemExtension(
context.Context,
io.ReadCloser,
details.ItemInfo,
*details.ExtensionInfo,
) (io.ReadCloser, error)
}
// AddItemExtensions wraps provided readcloser with extensions
// supplied via factory, with the first extension in slice being
// the innermost one.
func AddItemExtensions(
ctx context.Context,
rc io.ReadCloser,
info details.ItemInfo,
factories []CreateItemExtensioner,
) (io.ReadCloser, *details.ExtensionInfo, error) {
if rc == nil {
return nil, nil, clues.New("nil readcloser")
}
if len(factories) == 0 {
return nil, nil, clues.New("no extensions supplied")
}
ctx = clues.Add(ctx, "num_extensions", len(factories))
extInfo := &details.ExtensionInfo{
Data: make(map[string]any),
}
for _, factory := range factories {
if factory == nil {
return nil, nil, clues.New("nil extension factory")
}
extRc, err := factory.CreateItemExtension(ctx, rc, info, extInfo)
if err != nil {
return nil, nil, clues.Wrap(err, "create item extension")
}
rc = extRc
}
logger.Ctx(ctx).Debug("added item extensions")
return rc, extInfo, nil
}

View File

@ -0,0 +1,169 @@
package extensions
// Tests for extensions.go
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details"
)
type ExtensionsUnitSuite struct {
tester.Suite
}
func TestExtensionsUnitSuite(t *testing.T) {
suite.Run(t, &ExtensionsUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *ExtensionsUnitSuite) TestAddItemExtensions() {
type outputValidationFunc func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool
var (
testRc = io.NopCloser(bytes.NewReader([]byte("some data")))
testItemInfo = details.ItemInfo{
OneDrive: &details.OneDriveInfo{
DriveID: "driveID",
},
}
)
table := []struct {
name string
factories []CreateItemExtensioner
rc io.ReadCloser
validateOutputs outputValidationFunc
}{
{
name: "happy path",
factories: []CreateItemExtensioner{
&MockItemExtensionFactory{},
},
rc: testRc,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err == nil && extRc != nil && extInfo != nil
},
},
{
name: "multiple valid factories",
factories: []CreateItemExtensioner{
&MockItemExtensionFactory{},
&MockItemExtensionFactory{},
},
rc: testRc,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err == nil && extRc != nil && extInfo != nil
},
},
{
name: "no factories supplied",
factories: nil,
rc: testRc,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err != nil && extRc == nil && extInfo == nil
},
},
{
name: "factory slice contains nil",
factories: []CreateItemExtensioner{
&MockItemExtensionFactory{},
nil,
&MockItemExtensionFactory{},
},
rc: testRc,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err != nil && extRc == nil && extInfo == nil
},
},
{
name: "factory call returns error",
factories: []CreateItemExtensioner{
&MockItemExtensionFactory{
shouldReturnError: true,
},
},
rc: testRc,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err != nil && extRc == nil && extInfo == nil
},
},
{
name: "one or more factory calls return error",
factories: []CreateItemExtensioner{
&MockItemExtensionFactory{},
&MockItemExtensionFactory{
shouldReturnError: true,
},
},
rc: testRc,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err != nil && extRc == nil && extInfo == nil
},
},
{
name: "nil inner rc",
factories: []CreateItemExtensioner{
&MockItemExtensionFactory{},
},
rc: nil,
validateOutputs: func(
extRc io.ReadCloser,
extInfo *details.ExtensionInfo,
err error,
) bool {
return err != nil && extRc == nil && extInfo == nil
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
extRc, extInfo, err := AddItemExtensions(
ctx,
test.rc,
testItemInfo,
test.factories)
require.True(t, test.validateOutputs(extRc, extInfo, err))
})
}
}
// TODO(pandeyabs): Tests to verify RC wrapper ordering by AddItemExtensioner

View File

@ -0,0 +1,89 @@
package extensions
import (
"context"
"hash/crc32"
"io"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/logger"
)
var _ io.ReadCloser = &MockExtension{}
type MockExtension struct {
numBytes int
crc32 uint32
info details.ItemInfo
extInfo *details.ExtensionInfo
innerRc io.ReadCloser
ctx context.Context
failOnRead bool
failOnClose bool
}
func (me *MockExtension) Read(p []byte) (int, error) {
if me.failOnRead {
return 0, clues.New("mock read error")
}
n, err := me.innerRc.Read(p)
if err != nil && err != io.EOF {
logger.CtxErr(me.ctx, err).Error("inner read error")
return n, err
}
me.numBytes += n
me.crc32 = crc32.Update(me.crc32, crc32.IEEETable, p[:n])
if err == io.EOF {
logger.Ctx(me.ctx).Debug("mock extension reached EOF")
me.extInfo.Data["numBytes"] = me.numBytes
me.extInfo.Data["crc32"] = me.crc32
}
return n, err
}
func (me *MockExtension) Close() error {
if me.failOnClose {
return clues.New("mock close error")
}
err := me.innerRc.Close()
if err != nil {
return err
}
me.extInfo.Data["numBytes"] = me.numBytes
me.extInfo.Data["crc32"] = me.crc32
logger.Ctx(me.ctx).Infow(
"mock extension closed",
"numBytes", me.numBytes, "crc32", me.crc32)
return nil
}
type MockItemExtensionFactory struct {
shouldReturnError bool
}
func (m *MockItemExtensionFactory) CreateItemExtension(
ctx context.Context,
rc io.ReadCloser,
info details.ItemInfo,
extInfo *details.ExtensionInfo,
) (io.ReadCloser, error) {
if m.shouldReturnError {
return nil, clues.New("factory error")
}
return &MockExtension{
ctx: ctx,
innerRc: rc,
info: info,
extInfo: extInfo,
}, nil
}