corso/src/internal/kopia/data_collection_test.go
ashmrtn f0ccf35b5c
Inject versions in generic item (#4409)
Leverage the generic item struct to inject
serialization format information for all
items

Unwires the old code that injected versions
in kopia wrapper but leaves some code in
the wrapper to strip out the serialization
format during restore

Future PRs should move the process of
pulling out serialization format to
individual services

Viewing by commit may make review
easier

---

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

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

#### Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #4328

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
2023-09-29 20:16:01 +00:00

430 lines
10 KiB
Go

package kopia
import (
"bytes"
"context"
"io"
"testing"
"github.com/alcionai/clues"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/fs/virtualfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/readers"
"github.com/alcionai/corso/src/internal/data"
dataMock "github.com/alcionai/corso/src/internal/data/mock"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
)
// ---------------
// Wrappers to match required interfaces.
// ---------------
// These types are needed because we check that a fs.File was returned.
// Unfortunately fs.StreamingFile and fs.File have different interfaces so we
// have to fake things.
type mockSeeker struct{}
func (s mockSeeker) Seek(offset int64, whence int) (int64, error) {
return 0, clues.New("not implemented")
}
type mockReader struct {
io.ReadCloser
mockSeeker
}
func (r mockReader) Entry() (fs.Entry, error) {
return nil, clues.New("not implemented")
}
type mockFile struct {
// Use for Entry interface.
fs.StreamingFile
r io.ReadCloser
openErr error
size int64
}
func (f *mockFile) Open(ctx context.Context) (fs.Reader, error) {
if f.openErr != nil {
return nil, f.openErr
}
return mockReader{ReadCloser: f.r}, nil
}
func (f *mockFile) Size() int64 {
return f.size
}
// ---------------
// unit tests
// ---------------
type KopiaDataCollectionUnitSuite struct {
tester.Suite
}
func TestKopiaDataCollectionUnitSuite(t *testing.T) {
suite.Run(t, &KopiaDataCollectionUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *KopiaDataCollectionUnitSuite) TestReturnsPath() {
t := suite.T()
pth, err := path.Build(
"a-tenant",
"a-user",
path.ExchangeService,
path.EmailCategory,
false,
"some", "path", "for", "data")
require.NoError(t, err, clues.ToCore(err))
c := kopiaDataCollection{
path: pth,
}
assert.Equal(t, pth, c.FullPath())
}
func (suite *KopiaDataCollectionUnitSuite) TestReturnsStreams() {
type loadedData struct {
uuid string
data []byte
size int64
}
var (
fileData = [][]byte{
[]byte("abcdefghijklmnopqrstuvwxyz"),
[]byte("zyxwvutsrqponmlkjihgfedcba"),
}
uuids = []string{
"a-file",
"another-file",
}
files = []loadedData{
{uuid: uuids[0], data: fileData[0], size: int64(len(fileData[0]))},
{uuid: uuids[1], data: fileData[1], size: int64(len(fileData[1]))},
}
fileLookupErrName = "errLookup"
fileOpenErrName = "errOpen"
notFileErrName = "errNotFile"
)
// Needs to be a function so the readers get refreshed each time.
getLayout := func(t *testing.T) fs.Directory {
format := readers.SerializationFormat{
Version: readers.DefaultSerializationVersion,
}
r1, err := readers.NewVersionedBackupReader(
format,
io.NopCloser(bytes.NewReader(files[0].data)))
require.NoError(t, err, clues.ToCore(err))
r2, err := readers.NewVersionedBackupReader(
format,
io.NopCloser(bytes.NewReader(files[1].data)))
require.NoError(t, err, clues.ToCore(err))
return virtualfs.NewStaticDirectory(encodeAsPath("foo"), []fs.Entry{
&mockFile{
StreamingFile: virtualfs.StreamingFileFromReader(
encodeAsPath(files[0].uuid),
nil),
r: r1,
size: int64(len(files[0].data) + readers.VersionFormatSize),
},
&mockFile{
StreamingFile: virtualfs.StreamingFileFromReader(
encodeAsPath(files[1].uuid),
nil),
r: r2,
size: int64(len(files[1].data) + readers.VersionFormatSize),
},
&mockFile{
StreamingFile: virtualfs.StreamingFileFromReader(
encodeAsPath(fileOpenErrName),
nil),
openErr: assert.AnError,
},
virtualfs.NewStaticDirectory(encodeAsPath(notFileErrName), []fs.Entry{}),
})
}
table := []struct {
name string
uuidsAndErrors map[string]assert.ErrorAssertionFunc
// Data and stuff about the loaded data.
expectedLoaded []loadedData
}{
{
name: "SingleStream",
uuidsAndErrors: map[string]assert.ErrorAssertionFunc{
uuids[0]: nil,
},
expectedLoaded: []loadedData{files[0]},
},
{
name: "MultipleStreams",
uuidsAndErrors: map[string]assert.ErrorAssertionFunc{
uuids[0]: nil,
uuids[1]: nil,
},
expectedLoaded: files,
},
{
name: "Some Not Found Errors",
uuidsAndErrors: map[string]assert.ErrorAssertionFunc{
fileLookupErrName: assert.Error,
uuids[0]: nil,
},
expectedLoaded: []loadedData{files[0]},
},
{
name: "Some Not A File Errors",
uuidsAndErrors: map[string]assert.ErrorAssertionFunc{
notFileErrName: assert.Error,
uuids[0]: nil,
},
expectedLoaded: []loadedData{files[0]},
},
{
name: "Some Open Errors",
uuidsAndErrors: map[string]assert.ErrorAssertionFunc{
fileOpenErrName: assert.Error,
uuids[0]: nil,
},
expectedLoaded: []loadedData{files[0]},
},
{
name: "Empty Name Errors",
uuidsAndErrors: map[string]assert.ErrorAssertionFunc{
"": assert.Error,
},
expectedLoaded: []loadedData{},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
items := []string{}
errs := []assert.ErrorAssertionFunc{}
for uuid, err := range test.uuidsAndErrors {
if err != nil {
errs = append(errs, err)
}
items = append(items, uuid)
}
c := kopiaDataCollection{
dir: getLayout(t),
path: nil,
items: items,
expectedVersion: readers.DefaultSerializationVersion,
}
var (
found []loadedData
bus = fault.New(false)
)
for item := range c.Items(ctx, bus) {
require.Less(t, len(found), len(test.expectedLoaded), "items read safety")
found = append(found, loadedData{})
f := &found[len(found)-1]
f.uuid = item.ID()
buf, err := io.ReadAll(item.ToReader())
if !assert.NoError(t, err, clues.ToCore(err)) {
continue
}
f.data = buf
if !assert.Implements(t, (*data.ItemSize)(nil), item) {
continue
}
ss := item.(data.ItemSize)
f.size = ss.Size()
}
// We expect the items to be fetched in the order they are
// in the struct or the errors will not line up
for i, err := range bus.Recovered() {
assert.True(t, errs[i](t, err), "expected error", clues.ToCore(err))
}
assert.NoError(t, bus.Failure(), "expected no hard failures")
assert.ElementsMatch(t, test.expectedLoaded, found, "loaded items")
})
}
}
func (suite *KopiaDataCollectionUnitSuite) TestFetchItemByName() {
var (
tenant = "a-tenant"
user = "a-user"
category = path.EmailCategory
folder1 = "folder1"
folder2 = "folder2"
noErrFileName = "noError"
errFileName = "error"
errFileName2 = "error2"
noErrFileData = "foo bar baz"
errReader = &dataMock.Item{
ReadErr: assert.AnError,
}
)
// Needs to be a function so we can switch the serialization version as
// needed.
getLayout := func(
t *testing.T,
serVersion readers.SerializationVersion,
) fs.Directory {
format := readers.SerializationFormat{Version: serVersion}
r1, err := readers.NewVersionedBackupReader(
format,
io.NopCloser(bytes.NewReader([]byte(noErrFileData))))
require.NoError(t, err, clues.ToCore(err))
r2, err := readers.NewVersionedBackupReader(
format,
errReader.ToReader())
require.NoError(t, err, clues.ToCore(err))
return virtualfs.NewStaticDirectory(encodeAsPath(folder2), []fs.Entry{
&mockFile{
StreamingFile: virtualfs.StreamingFileFromReader(
encodeAsPath(noErrFileName),
nil),
r: r1,
},
&mockFile{
StreamingFile: virtualfs.StreamingFileFromReader(
encodeAsPath(errFileName),
nil),
r: r2,
},
&mockFile{
StreamingFile: virtualfs.StreamingFileFromReader(
encodeAsPath(errFileName2),
nil),
openErr: assert.AnError,
},
})
}
pth, err := path.Build(
tenant,
user,
path.ExchangeService,
category,
false,
folder1, folder2)
require.NoError(suite.T(), err, clues.ToCore(err))
table := []struct {
name string
inputName string
inputSerializationVersion readers.SerializationVersion
expectedData []byte
lookupErr assert.ErrorAssertionFunc
readErr assert.ErrorAssertionFunc
notFoundErr bool
}{
{
name: "FileFound_NoError",
inputName: noErrFileName,
inputSerializationVersion: readers.DefaultSerializationVersion,
expectedData: []byte(noErrFileData),
lookupErr: assert.NoError,
readErr: assert.NoError,
},
{
name: "FileFound_ReadError",
inputName: errFileName,
inputSerializationVersion: readers.DefaultSerializationVersion,
lookupErr: assert.NoError,
readErr: assert.Error,
},
{
name: "FileFound_VersionError",
inputName: noErrFileName,
inputSerializationVersion: readers.DefaultSerializationVersion + 1,
lookupErr: assert.Error,
},
{
name: "FileNotFound",
inputName: "foo",
inputSerializationVersion: readers.DefaultSerializationVersion + 1,
lookupErr: assert.Error,
notFoundErr: true,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
root := getLayout(t, test.inputSerializationVersion)
c := &i64counter{}
col := &kopiaDataCollection{
path: pth,
dir: root,
counter: c,
expectedVersion: readers.DefaultSerializationVersion,
}
s, err := col.FetchItemByName(ctx, test.inputName)
test.lookupErr(t, err)
if err != nil {
if test.notFoundErr {
assert.ErrorIs(t, err, data.ErrNotFound, clues.ToCore(err))
}
return
}
fileData, err := io.ReadAll(s.ToReader())
test.readErr(t, err, clues.ToCore(err))
if err != nil {
return
}
assert.Equal(t, test.expectedData, fileData)
})
}
}