Compare commits
5 Commits
main
...
3993-multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eb4cc16d0 | ||
|
|
c84b1b12ba | ||
|
|
7224edd6f6 | ||
|
|
b7867317c1 | ||
|
|
f504970adf |
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||
)
|
||||
|
||||
func AnyValueToT[T any](k string, m map[string]any) (T, error) {
|
||||
@ -23,3 +24,21 @@ func AnyValueToT[T any](k string, m map[string]any) (T, error) {
|
||||
|
||||
return vt, nil
|
||||
}
|
||||
|
||||
func AnyToT[T any](a any) (T, error) {
|
||||
if a == nil {
|
||||
return *new(T), clues.New("missing value")
|
||||
}
|
||||
|
||||
pt, ok := a.(*T)
|
||||
if ok {
|
||||
return ptr.Val(pt), nil
|
||||
}
|
||||
|
||||
t, ok := a.(T)
|
||||
if ok {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
return *new(T), clues.New(fmt.Sprintf("unexpected type: %T", a))
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ func (r reason) Category() path.CategoryType {
|
||||
}
|
||||
|
||||
func (r reason) SubtreePath() (path.Path, error) {
|
||||
p, err := path.ServicePrefix(
|
||||
p, err := path.BuildPrefix(
|
||||
r.Tenant(),
|
||||
r.ProtectedResource(),
|
||||
r.Service(),
|
||||
|
||||
@ -2865,16 +2865,16 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSelectsMigrateSubt
|
||||
migratedUser = "user_migrate"
|
||||
)
|
||||
|
||||
oldPrefixPathEmail, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
|
||||
oldPrefixPathEmail, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.EmailCategory)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
newPrefixPathEmail, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
|
||||
newPrefixPathEmail, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.EmailCategory)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
oldPrefixPathCont, err := path.ServicePrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
|
||||
oldPrefixPathCont, err := path.BuildPrefix(testTenant, testUser, path.ExchangeService, path.ContactsCategory)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
newPrefixPathCont, err := path.ServicePrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
|
||||
newPrefixPathCont, err := path.BuildPrefix(testTenant, migratedUser, path.ExchangeService, path.ContactsCategory)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
var (
|
||||
|
||||
@ -79,7 +79,7 @@ func BaseCollections(
|
||||
for cat := range categories {
|
||||
ictx := clues.Add(ctx, "base_service", service, "base_category", cat)
|
||||
|
||||
full, err := path.ServicePrefix(tenant, rOwner, service, cat)
|
||||
full, err := path.BuildPrefix(tenant, rOwner, service, cat)
|
||||
if err != nil {
|
||||
// Shouldn't happen.
|
||||
err = clues.Wrap(err, "making path").WithClues(ictx)
|
||||
|
||||
@ -24,10 +24,10 @@ func (suite *CollectionsUnitSuite) TestNewPrefixCollection() {
|
||||
serv := path.OneDriveService
|
||||
cat := path.FilesCategory
|
||||
|
||||
p1, err := path.ServicePrefix("t", "ro1", serv, cat)
|
||||
p1, err := path.BuildPrefix("t", "ro1", serv, cat)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
p2, err := path.ServicePrefix("t", "ro2", serv, cat)
|
||||
p2, err := path.BuildPrefix("t", "ro2", serv, cat)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
items, err := path.Build("t", "ro", serv, cat, true, "fld", "itm")
|
||||
|
||||
@ -109,7 +109,7 @@ func migrationCollections(
|
||||
|
||||
// unlike exchange, which enumerates all folders on every
|
||||
// backup, onedrive needs to force the owner PN -> ID migration
|
||||
mc, err := path.ServicePrefix(
|
||||
mc, err := path.BuildPrefix(
|
||||
tenant,
|
||||
bpc.ProtectedResource.ID(),
|
||||
path.OneDriveService,
|
||||
@ -118,7 +118,7 @@ func migrationCollections(
|
||||
return nil, clues.Wrap(err, "creating user id migration path")
|
||||
}
|
||||
|
||||
mpc, err := path.ServicePrefix(
|
||||
mpc, err := path.BuildPrefix(
|
||||
tenant,
|
||||
bpc.ProtectedResource.Name(),
|
||||
path.OneDriveService,
|
||||
|
||||
@ -42,7 +42,7 @@ func locationRef(
|
||||
|
||||
func basicLocationPath(repoRef path.Path, locRef *path.Builder) (path.Path, error) {
|
||||
if len(locRef.Elements()) == 0 {
|
||||
res, err := path.ServicePrefix(
|
||||
res, err := path.BuildPrefix(
|
||||
repoRef.Tenant(),
|
||||
repoRef.ResourceOwner(),
|
||||
repoRef.Service(),
|
||||
|
||||
@ -347,7 +347,7 @@ func testExchangeContinuousBackups(suite *ExchangeBackupIntgSuite, toggles contr
|
||||
// },
|
||||
}
|
||||
|
||||
rrPfx, err := path.ServicePrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
|
||||
rrPfx, err := path.BuildPrefix(acct.ID(), uidn.ID(), service, path.EmailCategory)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
// strip the category from the prefix; we primarily want the tenant and resource owner.
|
||||
|
||||
@ -213,7 +213,7 @@ func runDriveIncrementalTest(
|
||||
}
|
||||
)
|
||||
|
||||
rrPfx, err := path.ServicePrefix(atid, roidn.ID(), service, category)
|
||||
rrPfx, err := path.BuildPrefix(atid, roidn.ID(), service, category)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
// strip the category from the prefix; we primarily want the tenant and resource owner.
|
||||
|
||||
334
src/pkg/path/builder.go
Normal file
334
src/pkg/path/builder.go
Normal file
@ -0,0 +1,334 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
)
|
||||
|
||||
// interface compliance required for handling PII
|
||||
var (
|
||||
_ clues.Concealer = &Builder{}
|
||||
_ fmt.Stringer = &Builder{}
|
||||
)
|
||||
|
||||
// Builder is a simple path representation that only tracks path elements. It
|
||||
// can join, escape, and unescape elements. Higher-level packages are expected
|
||||
// to wrap this struct to build resource-specific contexts (e.x. an
|
||||
// ExchangeMailPath).
|
||||
// Resource-specific paths allow access to more information like segments in the
|
||||
// path. Builders that are turned into resource paths later on do not need to
|
||||
// manually add prefixes for items that normally appear in the data layer (ex.
|
||||
// tenant ID, service, user ID, etc).
|
||||
type Builder struct {
|
||||
// Unescaped version of elements.
|
||||
elements Elements
|
||||
}
|
||||
|
||||
// Append creates a copy of this Builder and adds the given elements to the
|
||||
// end of the new Builder. Elements are added in the order they are passed.
|
||||
func (pb Builder) Append(elements ...string) *Builder {
|
||||
res := &Builder{elements: make([]string, len(pb.elements))}
|
||||
copy(res.elements, pb.elements)
|
||||
|
||||
// Unescaped elements can't fail validation.
|
||||
//nolint:errcheck
|
||||
res.appendElements(false, elements)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (pb *Builder) appendElements(escaped bool, elements []string) error {
|
||||
for _, e := range elements {
|
||||
if len(e) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tmp := e
|
||||
|
||||
if escaped {
|
||||
tmp = TrimTrailingSlash(tmp)
|
||||
// If tmp was just the path separator then it will be empty now.
|
||||
if len(tmp) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateEscapedElement(tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp = unescape(tmp)
|
||||
}
|
||||
|
||||
pb.elements = append(pb.elements, tmp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
|
||||
// escaped path elements to the end of the new Builder. Elements are added in
|
||||
// the order they are passed.
|
||||
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
|
||||
res := &Builder{elements: make([]string, 0, len(pb.elements))}
|
||||
copy(res.elements, pb.elements)
|
||||
|
||||
if err := res.appendElements(true, elements); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SplitUnescapeAppend takes in an escaped string representing a directory
|
||||
// path, splits the string, and appends it to the current builder.
|
||||
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
|
||||
elems := Split(TrimTrailingSlash(s))
|
||||
|
||||
return pb.UnescapeAndAppend(elems...)
|
||||
}
|
||||
|
||||
func (pb Builder) PopFront() *Builder {
|
||||
if len(pb.elements) <= 1 {
|
||||
return &Builder{}
|
||||
}
|
||||
|
||||
elements := make([]string, len(pb.elements)-1)
|
||||
copy(elements, pb.elements[1:])
|
||||
|
||||
return &Builder{
|
||||
elements: elements,
|
||||
}
|
||||
}
|
||||
|
||||
// Dir removes the last element from the builder.
|
||||
func (pb Builder) Dir() *Builder {
|
||||
if len(pb.elements) <= 1 {
|
||||
return &Builder{}
|
||||
}
|
||||
|
||||
return &Builder{
|
||||
// Safe to use the same elements because Builders are immutable.
|
||||
elements: pb.elements[:len(pb.elements)-1],
|
||||
}
|
||||
}
|
||||
|
||||
// HeadElem returns the first element in the Builder.
|
||||
func (pb Builder) HeadElem() string {
|
||||
if len(pb.elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return pb.elements[0]
|
||||
}
|
||||
|
||||
// LastElem returns the last element in the Builder.
|
||||
func (pb Builder) LastElem() string {
|
||||
if len(pb.elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return pb.elements[len(pb.elements)-1]
|
||||
}
|
||||
|
||||
// UpdateParent updates leading elements matching prev to be cur and returns
|
||||
// true if it was updated. If prev is not a prefix of this Builder changes
|
||||
// nothing and returns false. If either prev or cur is nil does nothing and
|
||||
// returns false.
|
||||
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
|
||||
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
|
||||
return false
|
||||
}
|
||||
|
||||
parent := true
|
||||
|
||||
for i, e := range prev.Elements() {
|
||||
if pb.elements[i] != e {
|
||||
parent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !parent {
|
||||
return false
|
||||
}
|
||||
|
||||
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ShortRef produces a truncated hash of the builder that
|
||||
// acts as a unique identifier.
|
||||
func (pb Builder) ShortRef() string {
|
||||
if len(pb.elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
data := bytes.Buffer{}
|
||||
|
||||
for _, element := range pb.elements {
|
||||
data.WriteString(element)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(data.Bytes())
|
||||
|
||||
// Some conversions to get the right number of characters in the output. This
|
||||
// outputs hex, so we need to take the target number of characters and do the
|
||||
// equivalent of (shortRefCharacters * 4) / 8. This is
|
||||
// <number of bits represented> / <bits per byte> which gets us how many bytes
|
||||
// to give to our format command.
|
||||
numBytes := shortRefCharacters / 2
|
||||
|
||||
return fmt.Sprintf("%x", sum[:numBytes])
|
||||
}
|
||||
|
||||
// Elements returns all the elements in the path. This is a temporary function
|
||||
// and will likely be updated to handle encoded elements instead of clear-text
|
||||
// elements in the future.
|
||||
func (pb Builder) Elements() Elements {
|
||||
return append(Elements{}, pb.elements...)
|
||||
}
|
||||
|
||||
// withPrefix creates a Builder prefixed with the parameter values, and
|
||||
// concatenated with the current builder elements.
|
||||
func (pb Builder) withPrefix(elements ...string) *Builder {
|
||||
res := Builder{}.Append(elements...)
|
||||
res.elements = append(res.elements, pb.elements...)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data Layer Path Transformers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (pb Builder) ToStreamStorePath(
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
cat := DetailsCategory
|
||||
|
||||
if err := verifyPrefixValues(tenant, srs, cat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isItem && len(pb.elements) == 0 {
|
||||
return nil, clues.New("missing path beyond prefix")
|
||||
}
|
||||
|
||||
dlrp := newDataLayerResourcePath(pb, tenant, toMetadataServices(srs), cat, isItem)
|
||||
|
||||
return &dlrp, nil
|
||||
}
|
||||
|
||||
func (pb Builder) ToServiceCategoryMetadataPath(
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
cat CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
if err := verifyPrefixValues(tenant, srs, cat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isItem && len(pb.elements) == 0 {
|
||||
return nil, clues.New("missing path beyond prefix")
|
||||
}
|
||||
|
||||
dlrp := newDataLayerResourcePath(pb, tenant, toMetadataServices(srs), cat, isItem)
|
||||
|
||||
return &dlrp, nil
|
||||
}
|
||||
|
||||
func (pb Builder) ToDataLayerPath(
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
cat CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
if err := verifyPrefixValues(tenant, srs, cat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dlrp := newDataLayerResourcePath(pb, tenant, srs, cat, isItem)
|
||||
|
||||
return &dlrp, nil
|
||||
}
|
||||
|
||||
// TODO: remove this. https://github.com/alcionai/corso/issues/4025
|
||||
func (pb Builder) ToDataLayerExchangePathForCategory(
|
||||
tenant, mailboxID string,
|
||||
category CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
srs, err := NewServiceResources(ExchangeService, mailboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb.ToDataLayerPath(tenant, srs, category, isItem)
|
||||
}
|
||||
|
||||
// TODO: remove this. https://github.com/alcionai/corso/issues/4025
|
||||
func (pb Builder) ToDataLayerOneDrivePath(
|
||||
tenant, userID string,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
srs, err := NewServiceResources(OneDriveService, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb.ToDataLayerPath(tenant, srs, FilesCategory, isItem)
|
||||
}
|
||||
|
||||
// TODO: remove this. https://github.com/alcionai/corso/issues/4025
|
||||
func (pb Builder) ToDataLayerSharePointPath(
|
||||
tenant, siteID string,
|
||||
category CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
srs, err := NewServiceResources(SharePointService, siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb.ToDataLayerPath(tenant, srs, category, isItem)
|
||||
}
|
||||
|
||||
// TODO: ToDataLayerGroupsPath()
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stringers and PII Concealer Compliance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Conceal produces a concealed representation of the builder, suitable for
|
||||
// logging, storing in errors, and other output.
|
||||
func (pb Builder) Conceal() string {
|
||||
return pb.elements.Conceal()
|
||||
}
|
||||
|
||||
// Format produces a concealed representation of the builder, even when
|
||||
// used within a PrintF, suitable for logging, storing in errors,
|
||||
// and other output.
|
||||
func (pb Builder) Format(fs fmt.State, _ rune) {
|
||||
fmt.Fprint(fs, pb.Conceal())
|
||||
}
|
||||
|
||||
// String returns all path elements escaped and joined together as a single string.
|
||||
// The result is not concealed, and is not suitable for logging or structured
|
||||
// errors.
|
||||
func (pb Builder) String() string {
|
||||
return pb.elements.String()
|
||||
}
|
||||
|
||||
// PlainString returns an unescaped, unmodified string of the builder.
|
||||
// The result is not concealed, and is not suitable for logging or structured
|
||||
// errors.
|
||||
func (pb Builder) PlainString() string {
|
||||
return pb.elements.PlainString()
|
||||
}
|
||||
377
src/pkg/path/builder_test.go
Normal file
377
src/pkg/path/builder_test.go
Normal file
@ -0,0 +1,377 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
)
|
||||
|
||||
type BuilderUnitSuite struct {
|
||||
tester.Suite
|
||||
}
|
||||
|
||||
func TestBuilderUnitSuite(t *testing.T) {
|
||||
suite.Run(t, &BuilderUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||
}
|
||||
|
||||
// set the clues hashing to mask for the span of this suite
|
||||
func (suite *BuilderUnitSuite) SetupSuite() {
|
||||
clues.SetHasher(clues.HashCfg{HashAlg: clues.Flatmask})
|
||||
}
|
||||
|
||||
// revert clues hashing to plaintext for all other tests
|
||||
func (suite *BuilderUnitSuite) TeardownSuite() {
|
||||
clues.SetHasher(clues.NoHash())
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestAppend() {
|
||||
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p := Builder{}.Append(test.input...)
|
||||
assert.Equal(t, test.expectedString, p.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestAppendItem() {
|
||||
t := suite.T()
|
||||
|
||||
srs, err := NewServiceResources(ExchangeService, "ro")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
p, err := Build("t", srs, EmailCategory, false, "foo", "bar")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
pb := p.ToBuilder()
|
||||
assert.Equal(t, pb.String(), p.String())
|
||||
|
||||
pb = pb.Append("qux")
|
||||
|
||||
p, err = p.AppendItem("qux")
|
||||
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
assert.Equal(t, pb.String(), p.String())
|
||||
|
||||
_, err = p.AppendItem("fnords")
|
||||
require.Error(t, err, clues.ToCore(err))
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestUnescapeAndAppend() {
|
||||
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p, err := Builder{}.UnescapeAndAppend(test.input...)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
assert.Equal(t, test.expectedString, p.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestEscapedFailure() {
|
||||
target := "i_s"
|
||||
|
||||
for c := range charactersToEscape {
|
||||
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
|
||||
tmp := strings.ReplaceAll(target, "_", string(c))
|
||||
|
||||
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
||||
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestBadEscapeSequenceErrors() {
|
||||
target := `i\_s/a`
|
||||
notEscapes := []rune{'a', 'b', '#', '%'}
|
||||
|
||||
for _, c := range notEscapes {
|
||||
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
|
||||
tmp := strings.ReplaceAll(target, "_", string(c))
|
||||
|
||||
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
||||
assert.Errorf(
|
||||
suite.T(),
|
||||
err,
|
||||
"path with bad escape sequence %c%c did not error",
|
||||
escapeCharacter,
|
||||
c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestTrailingEscapeChar() {
|
||||
base := []string{"this", "is", "a", "path"}
|
||||
|
||||
for i := 0; i < len(base); i++ {
|
||||
suite.Run(fmt.Sprintf("Element%v", i), func() {
|
||||
path := make([]string, len(base))
|
||||
copy(path, base)
|
||||
path[i] = path[i] + string(escapeCharacter)
|
||||
|
||||
_, err := Builder{}.UnescapeAndAppend(path...)
|
||||
assert.Error(
|
||||
suite.T(),
|
||||
err,
|
||||
"path with trailing escape character did not error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestElements() {
|
||||
table := []struct {
|
||||
name string
|
||||
input []string
|
||||
output []string
|
||||
pathFunc func(elements []string) (*Builder, error)
|
||||
}{
|
||||
{
|
||||
name: "SimpleEscapedPath",
|
||||
input: []string{"this", "is", "a", "path"},
|
||||
output: []string{"this", "is", "a", "path"},
|
||||
pathFunc: func(elements []string) (*Builder, error) {
|
||||
return Builder{}.UnescapeAndAppend(elements...)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SimpleUnescapedPath",
|
||||
input: []string{"this", "is", "a", "path"},
|
||||
output: []string{"this", "is", "a", "path"},
|
||||
pathFunc: func(elements []string) (*Builder, error) {
|
||||
return Builder{}.Append(elements...), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EscapedPath",
|
||||
input: []string{"this", `is\/`, "a", "path"},
|
||||
output: []string{"this", "is/", "a", "path"},
|
||||
pathFunc: func(elements []string) (*Builder, error) {
|
||||
return Builder{}.UnescapeAndAppend(elements...)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p, err := test.pathFunc(test.input)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
assert.Equal(t, Elements(test.output), p.Elements())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestPopFront() {
|
||||
table := []struct {
|
||||
name string
|
||||
base *Builder
|
||||
expectedString string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
base: &Builder{},
|
||||
expectedString: "",
|
||||
},
|
||||
{
|
||||
name: "OneElement",
|
||||
base: Builder{}.Append("something"),
|
||||
expectedString: "",
|
||||
},
|
||||
{
|
||||
name: "TwoElements",
|
||||
base: Builder{}.Append("something", "else"),
|
||||
expectedString: "else",
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
assert.Equal(t, test.expectedString, test.base.PopFront().String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestShortRef() {
|
||||
table := []struct {
|
||||
name string
|
||||
inputElements []string
|
||||
expectedLen int
|
||||
}{
|
||||
{
|
||||
name: "PopulatedPath",
|
||||
inputElements: []string{"this", "is", "a", "path"},
|
||||
expectedLen: shortRefCharacters,
|
||||
},
|
||||
{
|
||||
name: "EmptyPath",
|
||||
inputElements: nil,
|
||||
expectedLen: 0,
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
pb := Builder{}.Append(test.inputElements...)
|
||||
ref := pb.ShortRef()
|
||||
assert.Len(suite.T(), ref, test.expectedLen)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestShortRefIsStable() {
|
||||
t := suite.T()
|
||||
pb := Builder{}.Append("this", "is", "a", "path")
|
||||
prevRef := pb.ShortRef()
|
||||
assert.Len(t, prevRef, shortRefCharacters)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
ref := pb.ShortRef()
|
||||
assert.Len(t, ref, shortRefCharacters)
|
||||
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
|
||||
|
||||
prevRef = ref
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestShortRefIsUnique() {
|
||||
pb1 := Builder{}.Append("this", "is", "a", "path")
|
||||
pb2 := pb1.Append("also")
|
||||
|
||||
require.NotEqual(suite.T(), pb1, pb2)
|
||||
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
||||
}
|
||||
|
||||
// TestShortRefUniqueWithEscaping tests that two paths that output the same
|
||||
// unescaped string but different escaped strings have different shortrefs. This
|
||||
// situation can occur when one path has embedded path separators while the
|
||||
// other does not but contains the same characters.
|
||||
func (suite *BuilderUnitSuite) TestShortRefUniqueWithEscaping() {
|
||||
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
|
||||
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
|
||||
|
||||
require.NotEqual(suite.T(), pb1, pb2)
|
||||
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestFolder() {
|
||||
table := []struct {
|
||||
name string
|
||||
p func(t *testing.T) Path
|
||||
escape bool
|
||||
expectFolder string
|
||||
expectSplit []string
|
||||
}{
|
||||
{
|
||||
name: "clean path",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
expectFolder: "a/b/c",
|
||||
expectSplit: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "clean path escaped",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
escape: true,
|
||||
expectFolder: "a/b/c",
|
||||
expectSplit: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "escapable path",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a/", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
expectFolder: "a//b/c",
|
||||
expectSplit: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "escapable path escaped",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a/", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
escape: true,
|
||||
expectFolder: "a\\//b/c",
|
||||
expectSplit: []string{"a\\/", "b", "c"},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p := test.p(t)
|
||||
result := p.Folder(test.escape)
|
||||
assert.Equal(t, test.expectFolder, result)
|
||||
assert.Equal(t, test.expectSplit, Split(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *BuilderUnitSuite) TestPIIHandling() {
|
||||
t := suite.T()
|
||||
|
||||
srs, err := NewServiceResources(ExchangeService, "ro")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
p, err := Build("t", srs, EventsCategory, true, "dir", "item")
|
||||
require.NoError(t, err)
|
||||
|
||||
table := []struct {
|
||||
name string
|
||||
p Path
|
||||
expect string
|
||||
expectPlain string
|
||||
}{
|
||||
{
|
||||
name: "standard path",
|
||||
p: p,
|
||||
expect: "***/exchange/***/events/***/***",
|
||||
expectPlain: "t/exchange/ro/events/dir/item",
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
|
||||
assert.Equal(t, test.expectPlain, test.p.String(), "string")
|
||||
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
|
||||
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
|
||||
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
|
||||
})
|
||||
}
|
||||
}
|
||||
90
src/pkg/path/category_type.go
Normal file
90
src/pkg/path/category_type.go
Normal file
@ -0,0 +1,90 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
)
|
||||
|
||||
var ErrorUnknownCategory = clues.New("unknown category string")
|
||||
|
||||
// CategoryType denotes what category of data the path corresponds to. The order
|
||||
// of the enums below can be changed, but the string representation of each enum
|
||||
// must remain the same or migration code needs to be added to handle changes to
|
||||
// the string format.
|
||||
type CategoryType int
|
||||
|
||||
//go:generate stringer -type=CategoryType -linecomment
|
||||
const (
|
||||
UnknownCategory CategoryType = iota
|
||||
EmailCategory // email
|
||||
ContactsCategory // contacts
|
||||
EventsCategory // events
|
||||
FilesCategory // files
|
||||
ListsCategory // lists
|
||||
LibrariesCategory // libraries
|
||||
PagesCategory // pages
|
||||
DetailsCategory // details
|
||||
)
|
||||
|
||||
func ToCategoryType(category string) CategoryType {
|
||||
cat := strings.ToLower(category)
|
||||
|
||||
switch cat {
|
||||
case strings.ToLower(EmailCategory.String()):
|
||||
return EmailCategory
|
||||
case strings.ToLower(ContactsCategory.String()):
|
||||
return ContactsCategory
|
||||
case strings.ToLower(EventsCategory.String()):
|
||||
return EventsCategory
|
||||
case strings.ToLower(FilesCategory.String()):
|
||||
return FilesCategory
|
||||
case strings.ToLower(LibrariesCategory.String()):
|
||||
return LibrariesCategory
|
||||
case strings.ToLower(ListsCategory.String()):
|
||||
return ListsCategory
|
||||
case strings.ToLower(PagesCategory.String()):
|
||||
return PagesCategory
|
||||
case strings.ToLower(DetailsCategory.String()):
|
||||
return DetailsCategory
|
||||
default:
|
||||
return UnknownCategory
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service-Category pairings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// serviceCategories is a mapping of all valid service/category pairs for
|
||||
// non-metadata paths.
|
||||
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
|
||||
ExchangeService: {
|
||||
EmailCategory: {},
|
||||
ContactsCategory: {},
|
||||
EventsCategory: {},
|
||||
},
|
||||
OneDriveService: {
|
||||
FilesCategory: {},
|
||||
},
|
||||
SharePointService: {
|
||||
LibrariesCategory: {},
|
||||
ListsCategory: {},
|
||||
PagesCategory: {},
|
||||
},
|
||||
}
|
||||
|
||||
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
|
||||
cats, ok := serviceCategories[service]
|
||||
if !ok {
|
||||
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
|
||||
}
|
||||
|
||||
if _, ok := cats[category]; !ok {
|
||||
return clues.New("unknown service/category combination").
|
||||
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -59,7 +59,10 @@ func (suite *OneDrivePathSuite) Test_ToOneDrivePath() {
|
||||
suite.Run(tt.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p, err := path.Build("tenant", "user", path.OneDriveService, path.FilesCategory, false, tt.pathElements...)
|
||||
srs, err := path.NewServiceResources(path.OneDriveService, "user")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
p, err := path.Build("tenant", srs, path.FilesCategory, false, tt.pathElements...)
|
||||
require.NoError(suite.T(), err, clues.ToCore(err))
|
||||
|
||||
got, err := path.ToDrivePath(p)
|
||||
|
||||
@ -51,8 +51,6 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -81,10 +79,12 @@ var (
|
||||
// string.
|
||||
type Path interface {
|
||||
String() string
|
||||
Service() ServiceType
|
||||
// ServiceResources produces all of the services and subservices, along with
|
||||
// the protected resource paired with the service, as contained in the path,
|
||||
// in their order of appearance.
|
||||
ServiceResources() []ServiceResource
|
||||
Category() CategoryType
|
||||
Tenant() string
|
||||
ResourceOwner() string
|
||||
Folder(escaped bool) string
|
||||
Folders() Elements
|
||||
Item() string
|
||||
@ -122,12 +122,6 @@ type Path interface {
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// interface compliance required for handling PII
|
||||
var (
|
||||
_ clues.Concealer = &Builder{}
|
||||
_ fmt.Stringer = &Builder{}
|
||||
)
|
||||
|
||||
// RestorePaths denotes the location to find an item in kopia and the path of
|
||||
// the collection to place the item in for restore.
|
||||
type RestorePaths struct {
|
||||
@ -135,396 +129,34 @@ type RestorePaths struct {
|
||||
RestorePath Path
|
||||
}
|
||||
|
||||
// Builder is a simple path representation that only tracks path elements. It
|
||||
// can join, escape, and unescape elements. Higher-level packages are expected
|
||||
// to wrap this struct to build resource-specific contexts (e.x. an
|
||||
// ExchangeMailPath).
|
||||
// Resource-specific paths allow access to more information like segments in the
|
||||
// path. Builders that are turned into resource paths later on do not need to
|
||||
// manually add prefixes for items that normally appear in the data layer (ex.
|
||||
// tenant ID, service, user ID, etc).
|
||||
type Builder struct {
|
||||
// Unescaped version of elements.
|
||||
elements Elements
|
||||
}
|
||||
|
||||
// Append creates a copy of this Builder and adds the given elements them to the
|
||||
// end of the new Builder. Elements are added in the order they are passed.
|
||||
func (pb Builder) Append(elements ...string) *Builder {
|
||||
res := &Builder{elements: make([]string, len(pb.elements))}
|
||||
copy(res.elements, pb.elements)
|
||||
|
||||
// Unescaped elements can't fail validation.
|
||||
//nolint:errcheck
|
||||
res.appendElements(false, elements)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (pb *Builder) appendElements(escaped bool, elements []string) error {
|
||||
for _, e := range elements {
|
||||
if len(e) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
tmp := e
|
||||
|
||||
if escaped {
|
||||
tmp = TrimTrailingSlash(tmp)
|
||||
// If tmp was just the path separator then it will be empty now.
|
||||
if len(tmp) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateEscapedElement(tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp = unescape(tmp)
|
||||
}
|
||||
|
||||
pb.elements = append(pb.elements, tmp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnescapeAndAppend creates a copy of this Builder and adds one or more already
|
||||
// escaped path elements to the end of the new Builder. Elements are added in
|
||||
// the order they are passed.
|
||||
func (pb Builder) UnescapeAndAppend(elements ...string) (*Builder, error) {
|
||||
res := &Builder{elements: make([]string, 0, len(pb.elements))}
|
||||
copy(res.elements, pb.elements)
|
||||
|
||||
if err := res.appendElements(true, elements); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SplitUnescapeAppend takes in an escaped string representing a directory
|
||||
// path, splits the string, and appends it to the current builder.
|
||||
func (pb Builder) SplitUnescapeAppend(s string) (*Builder, error) {
|
||||
elems := Split(TrimTrailingSlash(s))
|
||||
|
||||
return pb.UnescapeAndAppend(elems...)
|
||||
}
|
||||
|
||||
func (pb Builder) PopFront() *Builder {
|
||||
if len(pb.elements) <= 1 {
|
||||
return &Builder{}
|
||||
}
|
||||
|
||||
elements := make([]string, len(pb.elements)-1)
|
||||
copy(elements, pb.elements[1:])
|
||||
|
||||
return &Builder{
|
||||
elements: elements,
|
||||
}
|
||||
}
|
||||
|
||||
// Dir removes the last element from the builder.
|
||||
func (pb Builder) Dir() *Builder {
|
||||
if len(pb.elements) <= 1 {
|
||||
return &Builder{}
|
||||
}
|
||||
|
||||
return &Builder{
|
||||
// Safe to use the same elements because Builders are immutable.
|
||||
elements: pb.elements[:len(pb.elements)-1],
|
||||
}
|
||||
}
|
||||
|
||||
// HeadElem returns the first element in the Builder.
|
||||
func (pb Builder) HeadElem() string {
|
||||
if len(pb.elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return pb.elements[0]
|
||||
}
|
||||
|
||||
// LastElem returns the last element in the Builder.
|
||||
func (pb Builder) LastElem() string {
|
||||
if len(pb.elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return pb.elements[len(pb.elements)-1]
|
||||
}
|
||||
|
||||
// UpdateParent updates leading elements matching prev to be cur and returns
|
||||
// true if it was updated. If prev is not a prefix of this Builder changes
|
||||
// nothing and returns false. If either prev or cur is nil does nothing and
|
||||
// returns false.
|
||||
func (pb *Builder) UpdateParent(prev, cur *Builder) bool {
|
||||
if prev == cur || prev == nil || cur == nil || len(prev.Elements()) > len(pb.Elements()) {
|
||||
return false
|
||||
}
|
||||
|
||||
parent := true
|
||||
|
||||
for i, e := range prev.Elements() {
|
||||
if pb.elements[i] != e {
|
||||
parent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !parent {
|
||||
return false
|
||||
}
|
||||
|
||||
pb.elements = append(cur.Elements(), pb.elements[len(prev.Elements()):]...)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ShortRef produces a truncated hash of the builder that
|
||||
// acts as a unique identifier.
|
||||
func (pb Builder) ShortRef() string {
|
||||
if len(pb.elements) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
data := bytes.Buffer{}
|
||||
|
||||
for _, element := range pb.elements {
|
||||
data.WriteString(element)
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(data.Bytes())
|
||||
|
||||
// Some conversions to get the right number of characters in the output. This
|
||||
// outputs hex, so we need to take the target number of characters and do the
|
||||
// equivalent of (shortRefCharacters * 4) / 8. This is
|
||||
// <number of bits represented> / <bits per byte> which gets us how many bytes
|
||||
// to give to our format command.
|
||||
numBytes := shortRefCharacters / 2
|
||||
|
||||
return fmt.Sprintf("%x", sum[:numBytes])
|
||||
}
|
||||
|
||||
// Elements returns all the elements in the path. This is a temporary function
|
||||
// and will likely be updated to handle encoded elements instead of clear-text
|
||||
// elements in the future.
|
||||
func (pb Builder) Elements() Elements {
|
||||
return append(Elements{}, pb.elements...)
|
||||
}
|
||||
|
||||
func ServicePrefix(
|
||||
tenant, resourceOwner string,
|
||||
s ServiceType,
|
||||
c CategoryType,
|
||||
) (Path, error) {
|
||||
pb := Builder{}
|
||||
|
||||
if err := ValidateServiceAndCategory(s, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := verifyInputValues(tenant, resourceOwner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *pb.withPrefix(tenant, s.String(), resourceOwner, c.String()),
|
||||
service: s,
|
||||
category: c,
|
||||
hasItem: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// withPrefix creates a Builder prefixed with the parameter values, and
|
||||
// concatenated with the current builder elements.
|
||||
func (pb Builder) withPrefix(elements ...string) *Builder {
|
||||
res := Builder{}.Append(elements...)
|
||||
res.elements = append(res.elements, pb.elements...)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data Layer Path Transformers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (pb Builder) ToStreamStorePath(
|
||||
tenant, purpose string,
|
||||
service ServiceType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
if err := verifyInputValues(tenant, purpose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isItem && len(pb.elements) == 0 {
|
||||
return nil, clues.New("missing path beyond prefix")
|
||||
}
|
||||
|
||||
metadataService := UnknownService
|
||||
|
||||
switch service {
|
||||
case ExchangeService:
|
||||
metadataService = ExchangeMetadataService
|
||||
case OneDriveService:
|
||||
metadataService = OneDriveMetadataService
|
||||
case SharePointService:
|
||||
metadataService = SharePointMetadataService
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *pb.withPrefix(
|
||||
tenant,
|
||||
metadataService.String(),
|
||||
purpose,
|
||||
DetailsCategory.String()),
|
||||
service: metadataService,
|
||||
category: DetailsCategory,
|
||||
hasItem: isItem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pb Builder) ToServiceCategoryMetadataPath(
|
||||
tenant, user string,
|
||||
service ServiceType,
|
||||
category CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
if err := ValidateServiceAndCategory(service, category); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := verifyInputValues(tenant, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isItem && len(pb.elements) == 0 {
|
||||
return nil, clues.New("missing path beyond prefix")
|
||||
}
|
||||
|
||||
metadataService := UnknownService
|
||||
|
||||
switch service {
|
||||
case ExchangeService:
|
||||
metadataService = ExchangeMetadataService
|
||||
case OneDriveService:
|
||||
metadataService = OneDriveMetadataService
|
||||
case SharePointService:
|
||||
metadataService = SharePointMetadataService
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *pb.withPrefix(
|
||||
tenant,
|
||||
metadataService.String(),
|
||||
user,
|
||||
category.String(),
|
||||
),
|
||||
service: metadataService,
|
||||
category: category,
|
||||
hasItem: isItem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pb Builder) ToDataLayerPath(
|
||||
tenant, user string,
|
||||
service ServiceType,
|
||||
category CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
if err := ValidateServiceAndCategory(service, category); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pb.verifyPrefix(tenant, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *pb.withPrefix(
|
||||
tenant,
|
||||
service.String(),
|
||||
user,
|
||||
category.String()),
|
||||
service: service,
|
||||
category: category,
|
||||
hasItem: isItem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pb Builder) ToDataLayerExchangePathForCategory(
|
||||
tenant, user string,
|
||||
category CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
return pb.ToDataLayerPath(tenant, user, ExchangeService, category, isItem)
|
||||
}
|
||||
|
||||
func (pb Builder) ToDataLayerOneDrivePath(
|
||||
tenant, user string,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
return pb.ToDataLayerPath(tenant, user, OneDriveService, FilesCategory, isItem)
|
||||
}
|
||||
|
||||
func (pb Builder) ToDataLayerSharePointPath(
|
||||
tenant, site string,
|
||||
category CategoryType,
|
||||
isItem bool,
|
||||
) (Path, error) {
|
||||
return pb.ToDataLayerPath(tenant, site, SharePointService, category, isItem)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stringers and PII Concealer Compliance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Conceal produces a concealed representation of the builder, suitable for
|
||||
// logging, storing in errors, and other output.
|
||||
func (pb Builder) Conceal() string {
|
||||
return pb.elements.Conceal()
|
||||
}
|
||||
|
||||
// Format produces a concealed representation of the builder, even when
|
||||
// used within a PrintF, suitable for logging, storing in errors,
|
||||
// and other output.
|
||||
func (pb Builder) Format(fs fmt.State, _ rune) {
|
||||
fmt.Fprint(fs, pb.Conceal())
|
||||
}
|
||||
|
||||
// String returns a string that contains all path elements joined together.
|
||||
// Elements of the path that need escaping are escaped.
|
||||
// The result is not concealed, and is not suitable for logging or structured
|
||||
// errors.
|
||||
func (pb Builder) String() string {
|
||||
return pb.elements.String()
|
||||
}
|
||||
|
||||
// PlainString returns an unescaped, unmodified string of the builder.
|
||||
// The result is not concealed, and is not suitable for logging or structured
|
||||
// errors.
|
||||
func (pb Builder) PlainString() string {
|
||||
return pb.elements.PlainString()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exported Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func Build(
|
||||
tenant, resourceOwner string,
|
||||
service ServiceType,
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
category CategoryType,
|
||||
hasItem bool,
|
||||
elements ...string,
|
||||
) (Path, error) {
|
||||
b := Builder{}.Append(elements...)
|
||||
return Builder{}.
|
||||
Append(elements...).
|
||||
ToDataLayerPath(tenant, srs, category, hasItem)
|
||||
}
|
||||
|
||||
return b.ToDataLayerPath(
|
||||
tenant, resourceOwner,
|
||||
service, category,
|
||||
hasItem)
|
||||
func BuildPrefix(
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
cat CategoryType,
|
||||
) (Path, error) {
|
||||
if err := verifyPrefixValues(tenant, srs, cat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dlrp := newDataLayerResourcePath(Builder{}, tenant, srs, cat, false)
|
||||
|
||||
return &dlrp, nil
|
||||
}
|
||||
|
||||
// FromDataLayerPath parses the escaped path p, validates the elements in p
|
||||
@ -544,24 +176,37 @@ func FromDataLayerPath(p string, isItem bool) (Path, error) {
|
||||
return nil, clues.Stack(errParsingPath, err).With("path_string", p)
|
||||
}
|
||||
|
||||
// initial check for minimum required elements:
|
||||
// tenant, service, resource, category, container/item
|
||||
if len(pb.elements) < 5 {
|
||||
return nil, clues.New("path has too few segments").With("path_string", p)
|
||||
}
|
||||
|
||||
service, category, err := validateServiceAndCategoryStrings(
|
||||
pb.elements[1],
|
||||
pb.elements[3],
|
||||
)
|
||||
srs, catIdx, err := ElementsToServiceResources(pb.elements[1:])
|
||||
if err != nil {
|
||||
return nil, clues.Stack(err)
|
||||
}
|
||||
|
||||
// follow-up check: if more than one service exists, revisit the len check.
|
||||
if len(srs) > 1 && len(pb.elements) < 3+(2*len(srs)) {
|
||||
return nil, clues.New("path has too few segments").With("path_string", p)
|
||||
}
|
||||
|
||||
// +1 to account for slicing the tenant when calling the transformer func.
|
||||
category := ToCategoryType(pb.elements[catIdx+1])
|
||||
|
||||
if err := verifyPrefixValues(pb.elements[0], srs, category); err != nil {
|
||||
return nil, clues.Stack(errParsingPath, err).With("path_string", p)
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *pb,
|
||||
service: service,
|
||||
category: category,
|
||||
hasItem: isItem,
|
||||
}, nil
|
||||
dlrp := dataLayerResourcePath{
|
||||
Builder: *pb,
|
||||
serviceResources: srs,
|
||||
category: category,
|
||||
hasItem: isItem,
|
||||
}
|
||||
|
||||
return &dlrp, nil
|
||||
}
|
||||
|
||||
// TrimTrailingSlash takes an escaped path element and returns an escaped path
|
||||
@ -648,16 +293,21 @@ func Split(segment string) []string {
|
||||
// Unexported Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func verifyInputValues(tenant, resourceOwner string) error {
|
||||
func verifyPrefixValues(
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
cat CategoryType,
|
||||
) error {
|
||||
if len(tenant) == 0 {
|
||||
return clues.Stack(errMissingSegment, clues.New("tenant"))
|
||||
}
|
||||
|
||||
if len(resourceOwner) == 0 {
|
||||
return clues.Stack(errMissingSegment, clues.New("resourceOwner"))
|
||||
if err := validateServiceResources(srs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// only the final service is checked for its category validity
|
||||
return ValidateServiceAndCategory(srs[len(srs)-1].Service, cat)
|
||||
}
|
||||
|
||||
// escapeElement takes a single path element and escapes all characters that
|
||||
@ -762,17 +412,3 @@ func join(elements []string) string {
|
||||
// '\' according to the escaping rules.
|
||||
return strings.Join(elements, string(PathSeparator))
|
||||
}
|
||||
|
||||
// verifyPrefix ensures that the tenant and resourceOwner are valid
|
||||
// values, and that the builder has some directory structure.
|
||||
func (pb Builder) verifyPrefix(tenant, resourceOwner string) error {
|
||||
if err := verifyInputValues(tenant, resourceOwner); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pb.elements) == 0 {
|
||||
return clues.New("missing path beyond prefix")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
@ -233,239 +232,7 @@ func (suite *PathUnitSuite) TeardownSuite() {
|
||||
clues.SetHasher(clues.NoHash())
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestAppend() {
|
||||
table := append(append([]testData{}, genericCases...), basicUnescapedInputs...)
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p := Builder{}.Append(test.input...)
|
||||
assert.Equal(t, test.expectedString, p.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestAppendItem() {
|
||||
t := suite.T()
|
||||
|
||||
p, err := Build("t", "ro", ExchangeService, EmailCategory, false, "foo", "bar")
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
pb := p.ToBuilder()
|
||||
assert.Equal(t, pb.String(), p.String())
|
||||
|
||||
pb = pb.Append("qux")
|
||||
|
||||
p, err = p.AppendItem("qux")
|
||||
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
assert.Equal(t, pb.String(), p.String())
|
||||
|
||||
_, err = p.AppendItem("fnords")
|
||||
require.Error(t, err, clues.ToCore(err))
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestUnescapeAndAppend() {
|
||||
table := append(append([]testData{}, genericCases...), basicEscapedInputs...)
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p, err := Builder{}.UnescapeAndAppend(test.input...)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
assert.Equal(t, test.expectedString, p.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestEscapedFailure() {
|
||||
target := "i_s"
|
||||
|
||||
for c := range charactersToEscape {
|
||||
suite.Run(fmt.Sprintf("Unescaped-%c", c), func() {
|
||||
tmp := strings.ReplaceAll(target, "_", string(c))
|
||||
|
||||
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
||||
assert.Errorf(suite.T(), err, "path with unescaped %s did not error", string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestBadEscapeSequenceErrors() {
|
||||
target := `i\_s/a`
|
||||
notEscapes := []rune{'a', 'b', '#', '%'}
|
||||
|
||||
for _, c := range notEscapes {
|
||||
suite.Run(fmt.Sprintf("Escaped-%c", c), func() {
|
||||
tmp := strings.ReplaceAll(target, "_", string(c))
|
||||
|
||||
_, err := Builder{}.UnescapeAndAppend("this", tmp, "path")
|
||||
assert.Errorf(
|
||||
suite.T(),
|
||||
err,
|
||||
"path with bad escape sequence %c%c did not error",
|
||||
escapeCharacter,
|
||||
c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestTrailingEscapeChar() {
|
||||
base := []string{"this", "is", "a", "path"}
|
||||
|
||||
for i := 0; i < len(base); i++ {
|
||||
suite.Run(fmt.Sprintf("Element%v", i), func() {
|
||||
path := make([]string, len(base))
|
||||
copy(path, base)
|
||||
path[i] = path[i] + string(escapeCharacter)
|
||||
|
||||
_, err := Builder{}.UnescapeAndAppend(path...)
|
||||
assert.Error(
|
||||
suite.T(),
|
||||
err,
|
||||
"path with trailing escape character did not error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestElements() {
|
||||
table := []struct {
|
||||
name string
|
||||
input []string
|
||||
output []string
|
||||
pathFunc func(elements []string) (*Builder, error)
|
||||
}{
|
||||
{
|
||||
name: "SimpleEscapedPath",
|
||||
input: []string{"this", "is", "a", "path"},
|
||||
output: []string{"this", "is", "a", "path"},
|
||||
pathFunc: func(elements []string) (*Builder, error) {
|
||||
return Builder{}.UnescapeAndAppend(elements...)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SimpleUnescapedPath",
|
||||
input: []string{"this", "is", "a", "path"},
|
||||
output: []string{"this", "is", "a", "path"},
|
||||
pathFunc: func(elements []string) (*Builder, error) {
|
||||
return Builder{}.Append(elements...), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EscapedPath",
|
||||
input: []string{"this", `is\/`, "a", "path"},
|
||||
output: []string{"this", "is/", "a", "path"},
|
||||
pathFunc: func(elements []string) (*Builder, error) {
|
||||
return Builder{}.UnescapeAndAppend(elements...)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p, err := test.pathFunc(test.input)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
assert.Equal(t, Elements(test.output), p.Elements())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestPopFront() {
|
||||
table := []struct {
|
||||
name string
|
||||
base *Builder
|
||||
expectedString string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
base: &Builder{},
|
||||
expectedString: "",
|
||||
},
|
||||
{
|
||||
name: "OneElement",
|
||||
base: Builder{}.Append("something"),
|
||||
expectedString: "",
|
||||
},
|
||||
{
|
||||
name: "TwoElements",
|
||||
base: Builder{}.Append("something", "else"),
|
||||
expectedString: "else",
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
assert.Equal(t, test.expectedString, test.base.PopFront().String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestShortRef() {
|
||||
table := []struct {
|
||||
name string
|
||||
inputElements []string
|
||||
expectedLen int
|
||||
}{
|
||||
{
|
||||
name: "PopulatedPath",
|
||||
inputElements: []string{"this", "is", "a", "path"},
|
||||
expectedLen: shortRefCharacters,
|
||||
},
|
||||
{
|
||||
name: "EmptyPath",
|
||||
inputElements: nil,
|
||||
expectedLen: 0,
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
pb := Builder{}.Append(test.inputElements...)
|
||||
ref := pb.ShortRef()
|
||||
assert.Len(suite.T(), ref, test.expectedLen)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestShortRefIsStable() {
|
||||
t := suite.T()
|
||||
pb := Builder{}.Append("this", "is", "a", "path")
|
||||
prevRef := pb.ShortRef()
|
||||
assert.Len(t, prevRef, shortRefCharacters)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
ref := pb.ShortRef()
|
||||
assert.Len(t, ref, shortRefCharacters)
|
||||
assert.Equal(t, prevRef, ref, "ShortRef changed between calls")
|
||||
|
||||
prevRef = ref
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestShortRefIsUnique() {
|
||||
pb1 := Builder{}.Append("this", "is", "a", "path")
|
||||
pb2 := pb1.Append("also")
|
||||
|
||||
require.NotEqual(suite.T(), pb1, pb2)
|
||||
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
||||
}
|
||||
|
||||
// TestShortRefUniqueWithEscaping tests that two paths that output the same
|
||||
// unescaped string but different escaped strings have different shortrefs. This
|
||||
// situation can occur when one path has embedded path separators while the
|
||||
// other does not but contains the same characters.
|
||||
func (suite *PathUnitSuite) TestShortRefUniqueWithEscaping() {
|
||||
pb1 := Builder{}.Append(`this`, `is`, `a`, `path`)
|
||||
pb2 := Builder{}.Append(`this`, `is/a`, `path`)
|
||||
|
||||
require.NotEqual(suite.T(), pb1, pb2)
|
||||
assert.NotEqual(suite.T(), pb1.ShortRef(), pb2.ShortRef())
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestFromStringErrors() {
|
||||
func (suite *PathUnitSuite) TestFromDataLayerPathErrors() {
|
||||
table := []struct {
|
||||
name string
|
||||
escapedPath string
|
||||
@ -521,82 +288,7 @@ func (suite *PathUnitSuite) TestFromStringErrors() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestFolder() {
|
||||
table := []struct {
|
||||
name string
|
||||
p func(t *testing.T) Path
|
||||
escape bool
|
||||
expectFolder string
|
||||
expectSplit []string
|
||||
}{
|
||||
{
|
||||
name: "clean path",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
expectFolder: "a/b/c",
|
||||
expectSplit: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "clean path escaped",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
escape: true,
|
||||
expectFolder: "a/b/c",
|
||||
expectSplit: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "escapable path",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a/", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
expectFolder: "a//b/c",
|
||||
expectSplit: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "escapable path escaped",
|
||||
p: func(t *testing.T) Path {
|
||||
p, err := Builder{}.
|
||||
Append("a/", "b", "c").
|
||||
ToDataLayerExchangePathForCategory("t", "u", EmailCategory, false)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
return p
|
||||
},
|
||||
escape: true,
|
||||
expectFolder: "a\\//b/c",
|
||||
expectSplit: []string{"a\\/", "b", "c"},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
p := test.p(t)
|
||||
result := p.Folder(test.escape)
|
||||
assert.Equal(t, test.expectFolder, result)
|
||||
assert.Equal(t, test.expectSplit, Split(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestFromString() {
|
||||
func (suite *PathUnitSuite) TestFromDataLayerPath() {
|
||||
const (
|
||||
testTenant = "tenant"
|
||||
testUser = "user"
|
||||
@ -642,14 +334,12 @@ func (suite *PathUnitSuite) TestFromString() {
|
||||
testUser,
|
||||
testElement1,
|
||||
testElement2,
|
||||
testElement3,
|
||||
),
|
||||
testElement3),
|
||||
expectedFolder: fmt.Sprintf(
|
||||
"%s/%s/%s",
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
testElement3,
|
||||
),
|
||||
testElement3),
|
||||
expectedSplit: []string{
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
@ -659,8 +349,7 @@ func (suite *PathUnitSuite) TestFromString() {
|
||||
expectedItemFolder: fmt.Sprintf(
|
||||
"%s/%s",
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
),
|
||||
testElement2),
|
||||
expectedItemSplit: []string{
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
@ -674,14 +363,12 @@ func (suite *PathUnitSuite) TestFromString() {
|
||||
testUser,
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
testElement3,
|
||||
),
|
||||
testElement3),
|
||||
expectedFolder: fmt.Sprintf(
|
||||
"%s/%s/%s",
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
testElement3,
|
||||
),
|
||||
testElement3),
|
||||
expectedSplit: []string{
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
@ -691,8 +378,7 @@ func (suite *PathUnitSuite) TestFromString() {
|
||||
expectedItemFolder: fmt.Sprintf(
|
||||
"%s/%s",
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
),
|
||||
testElement2),
|
||||
expectedItemSplit: []string{
|
||||
testElementTrimmed,
|
||||
testElement2,
|
||||
@ -701,21 +387,27 @@ func (suite *PathUnitSuite) TestFromString() {
|
||||
}
|
||||
|
||||
for service, cats := range serviceCategories {
|
||||
|
||||
for cat := range cats {
|
||||
|
||||
for _, item := range isItem {
|
||||
suite.Run(fmt.Sprintf("%s-%s-%s", service, cat, item.name), func() {
|
||||
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
testPath := fmt.Sprintf(test.unescapedPath, service, cat)
|
||||
var (
|
||||
t = suite.T()
|
||||
testPath = fmt.Sprintf(test.unescapedPath, service, cat)
|
||||
sr = ServiceResource{service, testUser}
|
||||
)
|
||||
|
||||
p, err := FromDataLayerPath(testPath, item.isItem)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
assert.Equal(t, service, p.Service(), "service")
|
||||
assert.Len(t, p.ServiceResources(), 1, "service resources")
|
||||
assert.Equal(t, sr, p.ServiceResources()[0], "service resource")
|
||||
assert.Equal(t, cat, p.Category(), "category")
|
||||
assert.Equal(t, testTenant, p.Tenant(), "tenant")
|
||||
assert.Equal(t, testUser, p.ResourceOwner(), "resource owner")
|
||||
|
||||
fld := p.Folder(false)
|
||||
escfld := p.Folder(true)
|
||||
@ -740,77 +432,77 @@ func (suite *PathUnitSuite) TestFromString() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestPath_piiHandling() {
|
||||
p, err := Build("t", "ro", ExchangeService, EventsCategory, true, "dir", "item")
|
||||
require.NoError(suite.T(), err)
|
||||
|
||||
table := []struct {
|
||||
name string
|
||||
p Path
|
||||
expect string
|
||||
expectPlain string
|
||||
}{
|
||||
{
|
||||
name: "standard path",
|
||||
p: p,
|
||||
expect: "***/exchange/***/events/***/***",
|
||||
expectPlain: "t/exchange/ro/events/dir/item",
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
assert.Equal(t, test.expect, test.p.Conceal(), "conceal")
|
||||
assert.Equal(t, test.expectPlain, test.p.String(), "string")
|
||||
assert.Equal(t, test.expect, fmt.Sprintf("%s", test.p), "fmt %%s")
|
||||
assert.Equal(t, test.expect, fmt.Sprintf("%+v", test.p), "fmt %%+v")
|
||||
assert.Equal(t, test.expectPlain, test.p.PlainString(), "plain")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *PathUnitSuite) TestToServicePrefix() {
|
||||
func (suite *PathUnitSuite) TestBuildPrefix() {
|
||||
table := []struct {
|
||||
name string
|
||||
service ServiceType
|
||||
category CategoryType
|
||||
tenant string
|
||||
owner string
|
||||
srs []ServiceResource
|
||||
category CategoryType
|
||||
expect string
|
||||
expectErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
service: ExchangeService,
|
||||
category: ContactsCategory,
|
||||
tenant: "t",
|
||||
owner: "ro",
|
||||
expect: join([]string{"t", ExchangeService.String(), "ro", ContactsCategory.String()}),
|
||||
srs: []ServiceResource{{ExchangeService, "roo"}},
|
||||
category: ContactsCategory,
|
||||
expect: join([]string{"t", ExchangeService.String(), "roo", ContactsCategory.String()}),
|
||||
expectErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "ok with subservice",
|
||||
tenant: "t",
|
||||
srs: []ServiceResource{
|
||||
{GroupsService, "roo"},
|
||||
{SharePointService, "oor"},
|
||||
},
|
||||
category: LibrariesCategory,
|
||||
expect: join([]string{
|
||||
"t",
|
||||
GroupsService.String(), "roo",
|
||||
SharePointService.String(), "oor",
|
||||
LibrariesCategory.String()}),
|
||||
expectErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "bad category",
|
||||
service: ExchangeService,
|
||||
srs: []ServiceResource{{ExchangeService, "roo"}},
|
||||
category: FilesCategory,
|
||||
tenant: "t",
|
||||
owner: "ro",
|
||||
expectErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "bad tenant",
|
||||
service: ExchangeService,
|
||||
category: ContactsCategory,
|
||||
tenant: "",
|
||||
owner: "ro",
|
||||
srs: []ServiceResource{{ExchangeService, "roo"}},
|
||||
category: ContactsCategory,
|
||||
expectErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "bad owner",
|
||||
service: ExchangeService,
|
||||
category: ContactsCategory,
|
||||
name: "bad resource",
|
||||
tenant: "t",
|
||||
owner: "",
|
||||
srs: []ServiceResource{{ExchangeService, ""}},
|
||||
category: ContactsCategory,
|
||||
expectErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "bad subservice",
|
||||
tenant: "t",
|
||||
srs: []ServiceResource{
|
||||
{ExchangeService, "roo"},
|
||||
{OneDriveService, "oor"},
|
||||
},
|
||||
category: FilesCategory,
|
||||
expectErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "bad subservice resource",
|
||||
tenant: "t",
|
||||
srs: []ServiceResource{
|
||||
{GroupsService, "roo"},
|
||||
{SharePointService, ""},
|
||||
},
|
||||
category: LibrariesCategory,
|
||||
expectErr: require.Error,
|
||||
},
|
||||
}
|
||||
@ -818,7 +510,7 @@ func (suite *PathUnitSuite) TestToServicePrefix() {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
r, err := ServicePrefix(test.tenant, test.owner, test.service, test.category)
|
||||
r, err := BuildPrefix(test.tenant, test.srs, test.category)
|
||||
test.expectErr(t, err, clues.ToCore(err))
|
||||
|
||||
if r == nil {
|
||||
|
||||
@ -1,154 +1,9 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
)
|
||||
|
||||
var ErrorUnknownService = clues.New("unknown service string")
|
||||
|
||||
// ServiceType denotes what service the path corresponds to. Metadata services
|
||||
// are also included though they are only used for paths that house metadata for
|
||||
// Corso backups.
|
||||
//
|
||||
// Metadata services are not considered valid service types for resource paths
|
||||
// though they can be used for metadata paths.
|
||||
//
|
||||
// The order of the enums below can be changed, but the string representation of
|
||||
// each enum must remain the same or migration code needs to be added to handle
|
||||
// changes to the string format.
|
||||
type ServiceType int
|
||||
|
||||
//go:generate stringer -type=ServiceType -linecomment
|
||||
const (
|
||||
UnknownService ServiceType = iota
|
||||
ExchangeService // exchange
|
||||
OneDriveService // onedrive
|
||||
SharePointService // sharepoint
|
||||
ExchangeMetadataService // exchangeMetadata
|
||||
OneDriveMetadataService // onedriveMetadata
|
||||
SharePointMetadataService // sharepointMetadata
|
||||
)
|
||||
|
||||
func toServiceType(service string) ServiceType {
|
||||
s := strings.ToLower(service)
|
||||
|
||||
switch s {
|
||||
case strings.ToLower(ExchangeService.String()):
|
||||
return ExchangeService
|
||||
case strings.ToLower(OneDriveService.String()):
|
||||
return OneDriveService
|
||||
case strings.ToLower(SharePointService.String()):
|
||||
return SharePointService
|
||||
case strings.ToLower(ExchangeMetadataService.String()):
|
||||
return ExchangeMetadataService
|
||||
case strings.ToLower(OneDriveMetadataService.String()):
|
||||
return OneDriveMetadataService
|
||||
case strings.ToLower(SharePointMetadataService.String()):
|
||||
return SharePointMetadataService
|
||||
default:
|
||||
return UnknownService
|
||||
}
|
||||
}
|
||||
|
||||
var ErrorUnknownCategory = clues.New("unknown category string")
|
||||
|
||||
// CategoryType denotes what category of data the path corresponds to. The order
|
||||
// of the enums below can be changed, but the string representation of each enum
|
||||
// must remain the same or migration code needs to be added to handle changes to
|
||||
// the string format.
|
||||
type CategoryType int
|
||||
|
||||
//go:generate stringer -type=CategoryType -linecomment
|
||||
const (
|
||||
UnknownCategory CategoryType = iota
|
||||
EmailCategory // email
|
||||
ContactsCategory // contacts
|
||||
EventsCategory // events
|
||||
FilesCategory // files
|
||||
ListsCategory // lists
|
||||
LibrariesCategory // libraries
|
||||
PagesCategory // pages
|
||||
DetailsCategory // details
|
||||
)
|
||||
|
||||
func ToCategoryType(category string) CategoryType {
|
||||
cat := strings.ToLower(category)
|
||||
|
||||
switch cat {
|
||||
case strings.ToLower(EmailCategory.String()):
|
||||
return EmailCategory
|
||||
case strings.ToLower(ContactsCategory.String()):
|
||||
return ContactsCategory
|
||||
case strings.ToLower(EventsCategory.String()):
|
||||
return EventsCategory
|
||||
case strings.ToLower(FilesCategory.String()):
|
||||
return FilesCategory
|
||||
case strings.ToLower(LibrariesCategory.String()):
|
||||
return LibrariesCategory
|
||||
case strings.ToLower(ListsCategory.String()):
|
||||
return ListsCategory
|
||||
case strings.ToLower(PagesCategory.String()):
|
||||
return PagesCategory
|
||||
case strings.ToLower(DetailsCategory.String()):
|
||||
return DetailsCategory
|
||||
default:
|
||||
return UnknownCategory
|
||||
}
|
||||
}
|
||||
|
||||
// serviceCategories is a mapping of all valid service/category pairs for
|
||||
// non-metadata paths.
|
||||
var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
|
||||
ExchangeService: {
|
||||
EmailCategory: {},
|
||||
ContactsCategory: {},
|
||||
EventsCategory: {},
|
||||
},
|
||||
OneDriveService: {
|
||||
FilesCategory: {},
|
||||
},
|
||||
SharePointService: {
|
||||
LibrariesCategory: {},
|
||||
ListsCategory: {},
|
||||
PagesCategory: {},
|
||||
},
|
||||
}
|
||||
|
||||
func validateServiceAndCategoryStrings(s, c string) (ServiceType, CategoryType, error) {
|
||||
service := toServiceType(s)
|
||||
if service == UnknownService {
|
||||
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("service", fmt.Sprintf("%q", s))
|
||||
}
|
||||
|
||||
category := ToCategoryType(c)
|
||||
if category == UnknownCategory {
|
||||
return UnknownService, UnknownCategory, clues.Stack(ErrorUnknownService).With("category", fmt.Sprintf("%q", c))
|
||||
}
|
||||
|
||||
if err := ValidateServiceAndCategory(service, category); err != nil {
|
||||
return UnknownService, UnknownCategory, err
|
||||
}
|
||||
|
||||
return service, category, nil
|
||||
}
|
||||
|
||||
func ValidateServiceAndCategory(service ServiceType, category CategoryType) error {
|
||||
cats, ok := serviceCategories[service]
|
||||
if !ok {
|
||||
return clues.New("unsupported service").With("service", fmt.Sprintf("%q", service))
|
||||
}
|
||||
|
||||
if _, ok := cats[category]; !ok {
|
||||
return clues.New("unknown service/category combination").
|
||||
With("service", fmt.Sprintf("%q", service), "category", fmt.Sprintf("%q", category))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dataLayerResourcePath allows callers to extract information from a
|
||||
// resource-specific path. This struct is unexported so that callers are
|
||||
// forced to use the pre-defined constructors, making it impossible to create a
|
||||
@ -163,9 +18,28 @@ func ValidateServiceAndCategory(service ServiceType, category CategoryType) erro
|
||||
// element after the prefix.
|
||||
type dataLayerResourcePath struct {
|
||||
Builder
|
||||
category CategoryType
|
||||
service ServiceType
|
||||
hasItem bool
|
||||
category CategoryType
|
||||
serviceResources []ServiceResource
|
||||
hasItem bool
|
||||
}
|
||||
|
||||
// performs no validation, assumes the caller has validated the inputs.
|
||||
func newDataLayerResourcePath(
|
||||
pb Builder,
|
||||
tenant string,
|
||||
srs []ServiceResource,
|
||||
cat CategoryType,
|
||||
isItem bool,
|
||||
) dataLayerResourcePath {
|
||||
pfx := append([]string{tenant}, ServiceResourcesToElements(srs)...)
|
||||
pfx = append(pfx, cat.String())
|
||||
|
||||
return dataLayerResourcePath{
|
||||
Builder: *pb.withPrefix(pfx...),
|
||||
serviceResources: srs,
|
||||
category: cat,
|
||||
hasItem: isItem,
|
||||
}
|
||||
}
|
||||
|
||||
// Tenant returns the tenant ID embedded in the dataLayerResourcePath.
|
||||
@ -173,9 +47,8 @@ func (rp dataLayerResourcePath) Tenant() string {
|
||||
return rp.Builder.elements[0]
|
||||
}
|
||||
|
||||
// Service returns the ServiceType embedded in the dataLayerResourcePath.
|
||||
func (rp dataLayerResourcePath) Service() ServiceType {
|
||||
return rp.service
|
||||
func (rp dataLayerResourcePath) ServiceResources() []ServiceResource {
|
||||
return rp.serviceResources
|
||||
}
|
||||
|
||||
// Category returns the CategoryType embedded in the dataLayerResourcePath.
|
||||
@ -240,15 +113,18 @@ func (rp dataLayerResourcePath) Item() string {
|
||||
// Dir removes the last element from the path. If this would remove a
|
||||
// value that is part of the standard prefix structure, an error is returned.
|
||||
func (rp dataLayerResourcePath) Dir() (Path, error) {
|
||||
if len(rp.elements) <= 4 {
|
||||
// Dir is not allowed to slice off any prefix values.
|
||||
// The prefix len is determined by the length of the number of
|
||||
// service+resource tuples, plus 2 (tenant and category).
|
||||
if len(rp.elements) <= 2+(2*len(rp.serviceResources)) {
|
||||
return nil, clues.New("unable to shorten path").With("path", rp)
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *rp.Builder.Dir(),
|
||||
service: rp.service,
|
||||
category: rp.category,
|
||||
hasItem: false,
|
||||
Builder: *rp.Builder.Dir(),
|
||||
serviceResources: rp.serviceResources,
|
||||
category: rp.category,
|
||||
hasItem: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -261,10 +137,10 @@ func (rp dataLayerResourcePath) Append(
|
||||
}
|
||||
|
||||
return &dataLayerResourcePath{
|
||||
Builder: *rp.Builder.Append(elems...),
|
||||
service: rp.service,
|
||||
category: rp.category,
|
||||
hasItem: isItem,
|
||||
Builder: *rp.Builder.Append(elems...),
|
||||
serviceResources: rp.serviceResources,
|
||||
category: rp.category,
|
||||
hasItem: isItem,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -256,10 +256,10 @@ func (suite *DataLayerResourcePath) TestDir() {
|
||||
|
||||
func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
|
||||
tenant := "a-tenant"
|
||||
user := "a-user"
|
||||
resource := "a-resource"
|
||||
table := []struct {
|
||||
name string
|
||||
service path.ServiceType
|
||||
srs []path.ServiceResource
|
||||
category path.CategoryType
|
||||
postfix []string
|
||||
expectedService path.ServiceType
|
||||
@ -267,14 +267,14 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
|
||||
}{
|
||||
{
|
||||
name: "NoPostfixPasses",
|
||||
service: path.ExchangeService,
|
||||
srs: []path.ServiceResource{{path.ExchangeService, resource}},
|
||||
category: path.EmailCategory,
|
||||
expectedService: path.ExchangeMetadataService,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "PostfixPasses",
|
||||
service: path.ExchangeService,
|
||||
srs: []path.ServiceResource{{path.ExchangeService, resource}},
|
||||
category: path.EmailCategory,
|
||||
postfix: []string{"a", "b"},
|
||||
expectedService: path.ExchangeMetadataService,
|
||||
@ -282,48 +282,48 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
|
||||
},
|
||||
{
|
||||
name: "Fails",
|
||||
service: path.ExchangeService,
|
||||
srs: []path.ServiceResource{{path.ExchangeService, resource}},
|
||||
category: path.FilesCategory,
|
||||
check: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "Passes",
|
||||
service: path.ExchangeService,
|
||||
srs: []path.ServiceResource{{path.ExchangeService, resource}},
|
||||
category: path.ContactsCategory,
|
||||
expectedService: path.ExchangeMetadataService,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "Passes",
|
||||
service: path.ExchangeService,
|
||||
srs: []path.ServiceResource{{path.ExchangeService, resource}},
|
||||
category: path.EventsCategory,
|
||||
expectedService: path.ExchangeMetadataService,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "Passes",
|
||||
service: path.OneDriveService,
|
||||
srs: []path.ServiceResource{{path.OneDriveService, resource}},
|
||||
category: path.FilesCategory,
|
||||
expectedService: path.OneDriveMetadataService,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "Passes",
|
||||
service: path.SharePointService,
|
||||
srs: []path.ServiceResource{{path.SharePointService, resource}},
|
||||
category: path.LibrariesCategory,
|
||||
expectedService: path.SharePointMetadataService,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "Passes",
|
||||
service: path.SharePointService,
|
||||
srs: []path.ServiceResource{{path.SharePointService, resource}},
|
||||
category: path.ListsCategory,
|
||||
expectedService: path.SharePointMetadataService,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "Passes",
|
||||
service: path.SharePointService,
|
||||
srs: []path.ServiceResource{{path.SharePointService, resource}},
|
||||
category: path.PagesCategory,
|
||||
expectedService: path.SharePointMetadataService,
|
||||
check: assert.NoError,
|
||||
@ -331,27 +331,26 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
suite.Run(strings.Join([]string{
|
||||
name := strings.Join([]string{
|
||||
test.name,
|
||||
test.service.String(),
|
||||
test.srs[0].Service.String(),
|
||||
test.category.String(),
|
||||
}, "_"), func() {
|
||||
}, "_")
|
||||
|
||||
suite.Run(name, func() {
|
||||
t := suite.T()
|
||||
pb := path.Builder{}.Append(test.postfix...)
|
||||
|
||||
p, err := pb.ToServiceCategoryMetadataPath(
|
||||
tenant,
|
||||
user,
|
||||
test.service,
|
||||
test.srs,
|
||||
test.category,
|
||||
false)
|
||||
test.check(t, err, clues.ToCore(err))
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
if err == nil {
|
||||
assert.Equal(t, test.expectedService, p.ServiceResources()[0])
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expectedService, p.Service())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -402,9 +401,9 @@ func (suite *DataLayerResourcePath) TestToExchangePathForCategory() {
|
||||
}
|
||||
|
||||
assert.Equal(t, testTenant, p.Tenant())
|
||||
assert.Equal(t, path.ExchangeService, p.Service())
|
||||
assert.Equal(t, path.ExchangeService, p.ServiceResources()[0].Service)
|
||||
assert.Equal(t, test.category, p.Category())
|
||||
assert.Equal(t, testUser, p.ResourceOwner())
|
||||
assert.Equal(t, testUser, p.ServiceResources()[0].ProtectedResource)
|
||||
assert.Equal(t, strings.Join(m.expectedFolders, "/"), p.Folder(false))
|
||||
assert.Equal(t, path.Elements(m.expectedFolders), p.Folders())
|
||||
assert.Equal(t, m.expectedItem, p.Item())
|
||||
@ -456,7 +455,10 @@ func (suite *PopulatedDataLayerResourcePath) TestService() {
|
||||
suite.Run(m.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
assert.Equal(t, path.ExchangeService, suite.paths[m.isItem].Service())
|
||||
assert.Equal(
|
||||
t,
|
||||
path.ExchangeService,
|
||||
suite.paths[m.isItem].ServiceResources()[0].Service)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -476,7 +478,10 @@ func (suite *PopulatedDataLayerResourcePath) TestResourceOwner() {
|
||||
suite.Run(m.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
assert.Equal(t, testUser, suite.paths[m.isItem].ResourceOwner())
|
||||
assert.Equal(
|
||||
t,
|
||||
testUser,
|
||||
suite.paths[m.isItem].ServiceResources()[0].ProtectedResource)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,118 +20,76 @@ func TestServiceCategoryUnitSuite(t *testing.T) {
|
||||
suite.Run(t, s)
|
||||
}
|
||||
|
||||
func (suite *ServiceCategoryUnitSuite) TestValidateServiceAndCategoryBadStringErrors() {
|
||||
func (suite *ServiceCategoryUnitSuite) TestVerifyPrefixValues() {
|
||||
table := []struct {
|
||||
name string
|
||||
service string
|
||||
category string
|
||||
}{
|
||||
{
|
||||
name: "Service",
|
||||
service: "foo",
|
||||
category: EmailCategory.String(),
|
||||
},
|
||||
{
|
||||
name: "Category",
|
||||
service: ExchangeService.String(),
|
||||
category: "foo",
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
_, _, err := validateServiceAndCategoryStrings(test.service, test.category)
|
||||
assert.Error(suite.T(), err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ServiceCategoryUnitSuite) TestValidateServiceAndCategory() {
|
||||
table := []struct {
|
||||
name string
|
||||
service string
|
||||
category string
|
||||
expectedService ServiceType
|
||||
expectedCategory CategoryType
|
||||
check assert.ErrorAssertionFunc
|
||||
service ServiceType
|
||||
category CategoryType
|
||||
check assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "UnknownService",
|
||||
service: UnknownService.String(),
|
||||
category: EmailCategory.String(),
|
||||
service: UnknownService,
|
||||
category: EmailCategory,
|
||||
check: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "UnknownCategory",
|
||||
service: ExchangeService.String(),
|
||||
category: UnknownCategory.String(),
|
||||
service: ExchangeService,
|
||||
category: UnknownCategory,
|
||||
check: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "BadServiceString",
|
||||
service: "foo",
|
||||
category: EmailCategory.String(),
|
||||
name: "BadServiceType",
|
||||
service: ServiceType(-1),
|
||||
category: EmailCategory,
|
||||
check: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "BadCategoryString",
|
||||
service: ExchangeService.String(),
|
||||
category: "foo",
|
||||
name: "BadCategoryType",
|
||||
service: ExchangeService,
|
||||
category: CategoryType(-1),
|
||||
check: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "ExchangeEmail",
|
||||
service: ExchangeService.String(),
|
||||
category: EmailCategory.String(),
|
||||
expectedService: ExchangeService,
|
||||
expectedCategory: EmailCategory,
|
||||
check: assert.NoError,
|
||||
name: "ExchangeEmail",
|
||||
service: ExchangeService,
|
||||
category: EmailCategory,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "ExchangeContacts",
|
||||
service: ExchangeService.String(),
|
||||
category: ContactsCategory.String(),
|
||||
expectedService: ExchangeService,
|
||||
expectedCategory: ContactsCategory,
|
||||
check: assert.NoError,
|
||||
name: "ExchangeContacts",
|
||||
service: ExchangeService,
|
||||
category: ContactsCategory,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "ExchangeEvents",
|
||||
service: ExchangeService.String(),
|
||||
category: EventsCategory.String(),
|
||||
expectedService: ExchangeService,
|
||||
expectedCategory: EventsCategory,
|
||||
check: assert.NoError,
|
||||
name: "ExchangeEvents",
|
||||
service: ExchangeService,
|
||||
category: EventsCategory,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "OneDriveFiles",
|
||||
service: OneDriveService.String(),
|
||||
category: FilesCategory.String(),
|
||||
expectedService: OneDriveService,
|
||||
expectedCategory: FilesCategory,
|
||||
check: assert.NoError,
|
||||
name: "OneDriveFiles",
|
||||
service: OneDriveService,
|
||||
category: FilesCategory,
|
||||
check: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "SharePointLibraries",
|
||||
service: SharePointService.String(),
|
||||
category: LibrariesCategory.String(),
|
||||
expectedService: SharePointService,
|
||||
expectedCategory: LibrariesCategory,
|
||||
check: assert.NoError,
|
||||
name: "SharePointLibraries",
|
||||
service: SharePointService,
|
||||
category: LibrariesCategory,
|
||||
check: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
s, c, err := validateServiceAndCategoryStrings(test.service, test.category)
|
||||
srs := []ServiceResource{{test.service, "resource"}}
|
||||
|
||||
err := verifyPrefixValues("tid", srs, test.category)
|
||||
test.check(t, err, clues.ToCore(err))
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expectedService, s)
|
||||
assert.Equal(t, test.expectedCategory, c)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
174
src/pkg/path/service_resource.go
Normal file
174
src/pkg/path/service_resource.go
Normal file
@ -0,0 +1,174 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"github.com/alcionai/clues"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/common/str"
|
||||
"github.com/alcionai/corso/src/internal/common/tform"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tuple
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ServiceResource holds a service + resource tuple. The tuple implies
|
||||
// that the resource owns some data in the given service.
|
||||
type ServiceResource struct {
|
||||
Service ServiceType
|
||||
ProtectedResource string
|
||||
}
|
||||
|
||||
func MakeServiceResource(
|
||||
st ServiceType,
|
||||
protectedResource string,
|
||||
) ServiceResource {
|
||||
return ServiceResource{
|
||||
Service: st,
|
||||
ProtectedResource: protectedResource,
|
||||
}
|
||||
}
|
||||
|
||||
func (sr ServiceResource) validate() error {
|
||||
if len(sr.ProtectedResource) == 0 {
|
||||
return clues.Stack(errMissingSegment, clues.New("protected resource"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Collection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// NewServiceResources is a lenient constructor for building a
|
||||
// new []ServiceResource. It allows the caller to pass in any
|
||||
// number of arbitrary values, but will require the following:
|
||||
// 1. even values must be path.ServiceType typed
|
||||
// 2. odd values must be string typed
|
||||
// 3. a non-zero, even number of values must be provided
|
||||
func NewServiceResources(elems ...any) ([]ServiceResource, error) {
|
||||
if len(elems) == 0 {
|
||||
return nil, clues.New("missing service resources")
|
||||
}
|
||||
|
||||
if len(elems)%2 == 1 {
|
||||
return nil, clues.New("odd number of service resources")
|
||||
}
|
||||
|
||||
srs := make([]ServiceResource, 0, len(elems)/2)
|
||||
|
||||
for i, j := 0, 1; i < len(elems); i, j = i+2, j+2 {
|
||||
srv, err := tform.AnyToT[ServiceType](elems[i])
|
||||
if err != nil {
|
||||
return nil, clues.Wrap(err, "service")
|
||||
}
|
||||
|
||||
pr, err := str.AnyToString(elems[j])
|
||||
if err != nil {
|
||||
return nil, clues.Wrap(err, "protected resource")
|
||||
}
|
||||
|
||||
srs = append(srs, MakeServiceResource(srv, pr))
|
||||
}
|
||||
|
||||
return srs, nil
|
||||
}
|
||||
|
||||
func ServiceResourcesToElements(srs []ServiceResource) Elements {
|
||||
es := make(Elements, 0, len(srs)*2)
|
||||
|
||||
for _, tuple := range srs {
|
||||
es = append(es, tuple.Service.String())
|
||||
es = append(es, tuple.ProtectedResource)
|
||||
}
|
||||
|
||||
return es
|
||||
}
|
||||
|
||||
// ElementsToServiceResources turns as many pairs of elems as possible
|
||||
// into ServiceResource tuples. Elems must begin with a service, but
|
||||
// may contain more entries than there are service/resource pairs.
|
||||
// This transformer will continue consuming elements until it finds an
|
||||
// even-numbered index that cannot be cast to a ServiceType.
|
||||
// Returns the serviceResource pairs, the first index containing element
|
||||
// that is not part of a service/resource pair, and an error if elems is
|
||||
// len==0 or contains no services.
|
||||
func ElementsToServiceResources(elems Elements) ([]ServiceResource, int, error) {
|
||||
if len(elems) == 0 {
|
||||
return nil, -1, clues.Wrap(errMissingSegment, "service")
|
||||
}
|
||||
|
||||
var (
|
||||
srs = make([]ServiceResource, 0)
|
||||
i int
|
||||
)
|
||||
|
||||
for j := 1; i < len(elems); i, j = i+2, j+2 {
|
||||
service := toServiceType(elems[i])
|
||||
if service == UnknownService {
|
||||
if i == 0 {
|
||||
return nil, -1, clues.Wrap(errMissingSegment, "service")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
srs = append(srs, ServiceResource{service, elems[j]})
|
||||
}
|
||||
|
||||
return srs, i, nil
|
||||
}
|
||||
|
||||
// checks for the following:
|
||||
// 1. each ServiceResource is valid
|
||||
// 2. if len(srs) > 1, srs[i], srs[i+1] pass subservice checks.
|
||||
func validateServiceResources(srs []ServiceResource) error {
|
||||
switch len(srs) {
|
||||
case 0:
|
||||
return clues.Stack(errMissingSegment, clues.New("service"))
|
||||
case 1:
|
||||
return srs[0].validate()
|
||||
}
|
||||
|
||||
for i, tuple := range srs {
|
||||
if err := tuple.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i+1 >= len(srs) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ValidateServiceAndSubService(tuple.Service, srs[i+1].Service); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makes a copy of the slice with all of the Services swapped for their
|
||||
// metadata service countterparts.
|
||||
func toMetadataServices(srs []ServiceResource) []ServiceResource {
|
||||
msrs := make([]ServiceResource, 0, len(srs))
|
||||
|
||||
for _, sr := range srs {
|
||||
msr := sr
|
||||
metadataService := UnknownService
|
||||
|
||||
switch sr.Service {
|
||||
case ExchangeService:
|
||||
metadataService = ExchangeMetadataService
|
||||
case OneDriveService:
|
||||
metadataService = OneDriveMetadataService
|
||||
case SharePointService:
|
||||
metadataService = SharePointMetadataService
|
||||
// TODO: add groups
|
||||
}
|
||||
|
||||
msr.Service = metadataService
|
||||
msrs = append(msrs, msr)
|
||||
}
|
||||
|
||||
return msrs
|
||||
}
|
||||
264
src/pkg/path/service_resource_test.go
Normal file
264
src/pkg/path/service_resource_test.go
Normal file
@ -0,0 +1,264 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
)
|
||||
|
||||
type ServiceResourceUnitSuite struct {
|
||||
tester.Suite
|
||||
}
|
||||
|
||||
func TestServiceResourceUnitSuite(t *testing.T) {
|
||||
suite.Run(t, &ServiceResourceUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||
}
|
||||
|
||||
func (suite *ServiceResourceUnitSuite) TestNewServiceResource() {
|
||||
table := []struct {
|
||||
name string
|
||||
input []any
|
||||
expectErr assert.ErrorAssertionFunc
|
||||
expectResult []ServiceResource
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
input: []any{},
|
||||
expectErr: assert.Error,
|
||||
expectResult: nil,
|
||||
},
|
||||
{
|
||||
name: "odd elems: 1",
|
||||
input: []any{ExchangeService},
|
||||
expectErr: assert.Error,
|
||||
expectResult: nil,
|
||||
},
|
||||
{
|
||||
name: "odd elems: 3",
|
||||
input: []any{ExchangeService, "mailbox", OneDriveService},
|
||||
expectErr: assert.Error,
|
||||
expectResult: nil,
|
||||
},
|
||||
{
|
||||
name: "non-service even index",
|
||||
input: []any{"foo", "bar"},
|
||||
expectErr: assert.Error,
|
||||
expectResult: nil,
|
||||
},
|
||||
{
|
||||
name: "non-string odd index",
|
||||
input: []any{ExchangeService, OneDriveService},
|
||||
expectErr: assert.Error,
|
||||
expectResult: nil,
|
||||
},
|
||||
{
|
||||
name: "valid single",
|
||||
input: []any{ExchangeService, "mailbox"},
|
||||
expectErr: assert.NoError,
|
||||
expectResult: []ServiceResource{{ExchangeService, "mailbox"}},
|
||||
},
|
||||
{
|
||||
name: "valid multiple",
|
||||
input: []any{ExchangeService, "mailbox", OneDriveService, "user"},
|
||||
expectErr: assert.NoError,
|
||||
expectResult: []ServiceResource{
|
||||
{ExchangeService, "mailbox"},
|
||||
{OneDriveService, "user"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
result, err := NewServiceResources(test.input...)
|
||||
test.expectErr(t, err, clues.ToCore(err))
|
||||
assert.Equal(t, test.expectResult, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ServiceResourceUnitSuite) TestValidateServiceResources() {
|
||||
table := []struct {
|
||||
name string
|
||||
srs []ServiceResource
|
||||
expect assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
srs: []ServiceResource{},
|
||||
expect: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "invalid resource",
|
||||
srs: []ServiceResource{{ExchangeService, ""}},
|
||||
expect: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "invalid subservice",
|
||||
srs: []ServiceResource{
|
||||
{ExchangeService, "mailbox"},
|
||||
{OneDriveService, "user"},
|
||||
},
|
||||
expect: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
srs: []ServiceResource{
|
||||
{GroupsService, "group"},
|
||||
{SharePointService, "site"},
|
||||
},
|
||||
expect: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
err := validateServiceResources(test.srs)
|
||||
test.expect(t, err, clues.ToCore(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ServiceResourceUnitSuite) TestServiceResourceToElements() {
|
||||
table := []struct {
|
||||
name string
|
||||
srs []ServiceResource
|
||||
expect Elements
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
srs: []ServiceResource{},
|
||||
expect: Elements{},
|
||||
},
|
||||
{
|
||||
name: "single",
|
||||
srs: []ServiceResource{{ExchangeService, "user"}},
|
||||
expect: Elements{ExchangeService.String(), "user"},
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
srs: []ServiceResource{
|
||||
{ExchangeService, "mailbox"},
|
||||
{OneDriveService, "user"},
|
||||
},
|
||||
expect: Elements{
|
||||
ExchangeService.String(), "mailbox",
|
||||
OneDriveService.String(), "user",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
result := ServiceResourcesToElements(test.srs)
|
||||
|
||||
// not ElementsMatch, order matters
|
||||
assert.Equal(t, test.expect, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ServiceResourceUnitSuite) TestElementsToServiceResource() {
|
||||
table := []struct {
|
||||
name string
|
||||
elems Elements
|
||||
expectErr assert.ErrorAssertionFunc
|
||||
expectIdx int
|
||||
expectSRS []ServiceResource
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
elems: Elements{},
|
||||
expectErr: assert.Error,
|
||||
expectIdx: -1,
|
||||
expectSRS: nil,
|
||||
},
|
||||
{
|
||||
name: "nil",
|
||||
elems: nil,
|
||||
expectErr: assert.Error,
|
||||
expectIdx: -1,
|
||||
expectSRS: nil,
|
||||
},
|
||||
{
|
||||
name: "non-service 0th elem",
|
||||
elems: Elements{"fnords"},
|
||||
expectErr: assert.Error,
|
||||
expectIdx: -1,
|
||||
expectSRS: nil,
|
||||
},
|
||||
{
|
||||
name: "non-service 2nd elem",
|
||||
elems: Elements{ExchangeService.String(), "fnords", "smarf"},
|
||||
expectErr: assert.Error,
|
||||
expectIdx: -1,
|
||||
expectSRS: nil,
|
||||
},
|
||||
{
|
||||
name: "single serviceResource",
|
||||
elems: Elements{ExchangeService.String(), "fnords"},
|
||||
expectErr: assert.NoError,
|
||||
expectIdx: 2,
|
||||
expectSRS: []ServiceResource{{ExchangeService, "fnords"}},
|
||||
},
|
||||
{
|
||||
name: "single serviceResource and extra value",
|
||||
elems: Elements{ExchangeService.String(), "fnords", "smarf"},
|
||||
expectErr: assert.NoError,
|
||||
expectIdx: 2,
|
||||
expectSRS: []ServiceResource{{ExchangeService, "fnords"}},
|
||||
},
|
||||
{
|
||||
name: "multiple serviceResource",
|
||||
elems: Elements{ExchangeService.String(), "fnords", OneDriveService.String(), "smarf"},
|
||||
expectErr: assert.NoError,
|
||||
expectIdx: 4,
|
||||
expectSRS: []ServiceResource{{ExchangeService, "fnords"}, {OneDriveService, "smarf"}},
|
||||
},
|
||||
{
|
||||
name: "multiple serviceResource and extra value",
|
||||
elems: Elements{ExchangeService.String(), "fnords", OneDriveService.String(), "smarf", "flaboigans"},
|
||||
expectErr: assert.NoError,
|
||||
expectIdx: 4,
|
||||
expectSRS: []ServiceResource{{ExchangeService, "fnords"}, {OneDriveService, "smarf"}},
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
srs, idx, err := ElementsToServiceResources(test.elems)
|
||||
test.expectErr(t, err, clues.ToCore(err))
|
||||
assert.Equal(t, test.expectIdx, idx)
|
||||
assert.Equal(t, test.expectSRS, srs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ServiceResourceUnitSuite) TestToMetadataServices() {
|
||||
srs := []ServiceResource{
|
||||
{ExchangeService, "mailbox"},
|
||||
{OneDriveService, "user"},
|
||||
{SharePointService, "site"},
|
||||
{GroupsService, "group"},
|
||||
}
|
||||
|
||||
expect := []ServiceResource{
|
||||
{ExchangeMetadataService, "mailbox"},
|
||||
{OneDriveMetadataService, "user"},
|
||||
{SharePointMetadataService, "site"},
|
||||
{GroupsMetadataService, "group"},
|
||||
}
|
||||
|
||||
assert.Equal(
|
||||
suite.T(),
|
||||
expect,
|
||||
toMetadataServices(srs))
|
||||
}
|
||||
88
src/pkg/path/service_type.go
Normal file
88
src/pkg/path/service_type.go
Normal file
@ -0,0 +1,88 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
)
|
||||
|
||||
var ErrorUnknownService = clues.New("unknown service string")
|
||||
|
||||
// ServiceType denotes what service the path corresponds to. Metadata services
|
||||
// are also included though they are only used for paths that house metadata for
|
||||
// Corso backups.
|
||||
//
|
||||
// Metadata services are not considered valid service types for resource paths
|
||||
// though they can be used for metadata paths.
|
||||
//
|
||||
// The order of the enums below can be changed, but the string representation of
|
||||
// each enum must remain the same or migration code needs to be added to handle
|
||||
// changes to the string format.
|
||||
type ServiceType int
|
||||
|
||||
//go:generate stringer -type=ServiceType -linecomment
|
||||
const (
|
||||
UnknownService ServiceType = iota
|
||||
ExchangeService // exchange
|
||||
OneDriveService // onedrive
|
||||
SharePointService // sharepoint
|
||||
ExchangeMetadataService // exchangeMetadata
|
||||
OneDriveMetadataService // onedriveMetadata
|
||||
SharePointMetadataService // sharepointMetadata
|
||||
GroupsService // groups
|
||||
GroupsMetadataService // groupsMetadata
|
||||
)
|
||||
|
||||
func toServiceType(service string) ServiceType {
|
||||
s := strings.ToLower(service)
|
||||
|
||||
switch s {
|
||||
case strings.ToLower(ExchangeService.String()):
|
||||
return ExchangeService
|
||||
case strings.ToLower(OneDriveService.String()):
|
||||
return OneDriveService
|
||||
case strings.ToLower(SharePointService.String()):
|
||||
return SharePointService
|
||||
case strings.ToLower(ExchangeMetadataService.String()):
|
||||
return ExchangeMetadataService
|
||||
case strings.ToLower(OneDriveMetadataService.String()):
|
||||
return OneDriveMetadataService
|
||||
case strings.ToLower(SharePointMetadataService.String()):
|
||||
return SharePointMetadataService
|
||||
default:
|
||||
return UnknownService
|
||||
}
|
||||
}
|
||||
|
||||
// subServices is a mapping of all valid service/subService pairs.
|
||||
// a subService pair occurs when one service contains a reference
|
||||
// to a protected resource of another service type, and the resource
|
||||
// for that second service is the identifier which is used to discover
|
||||
// data. A subService relationship may imply that the subservice data
|
||||
// is wholly replicated/owned by the primary service, or it may not,
|
||||
// each case differs.
|
||||
//
|
||||
// Ex:
|
||||
// - groups/<gID>/sharepoint/<siteID> => each team in groups contains a
|
||||
// complete sharepoint site.
|
||||
// - groups/<gID>/member/<userID> => each user in a team can own one or
|
||||
// more Chats. But the group does not contain the complete user data.
|
||||
var subServices = map[ServiceType]map[ServiceType]struct{}{
|
||||
GroupsService: {
|
||||
SharePointService: {},
|
||||
},
|
||||
}
|
||||
|
||||
func ValidateServiceAndSubService(service, subService ServiceType) error {
|
||||
subs, ok := subServices[service]
|
||||
if !ok {
|
||||
return clues.New("unsupported service").With("service", service)
|
||||
}
|
||||
|
||||
if _, ok := subs[subService]; !ok {
|
||||
return clues.New("unknown service/subService combination").
|
||||
With("service", service, "subService", subService)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
54
src/pkg/path/service_type_test.go
Normal file
54
src/pkg/path/service_type_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package path
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
)
|
||||
|
||||
type ServiceTypeUnitSuite struct {
|
||||
tester.Suite
|
||||
}
|
||||
|
||||
func TestServiceTypeUnitSuite(t *testing.T) {
|
||||
suite.Run(t, &ServiceTypeUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||
}
|
||||
|
||||
var knownServices = []ServiceType{
|
||||
UnknownService,
|
||||
ExchangeService,
|
||||
OneDriveService,
|
||||
SharePointService,
|
||||
ExchangeMetadataService,
|
||||
OneDriveMetadataService,
|
||||
SharePointMetadataService,
|
||||
GroupsService,
|
||||
GroupsMetadataService,
|
||||
}
|
||||
|
||||
func (suite *ServiceTypeUnitSuite) TestValildateServiceAndSubService() {
|
||||
table := map[ServiceType]map[ServiceType]assert.ErrorAssertionFunc{}
|
||||
|
||||
for _, si := range knownServices {
|
||||
table[si] = map[ServiceType]assert.ErrorAssertionFunc{}
|
||||
for _, sj := range knownServices {
|
||||
table[si][sj] = assert.Error
|
||||
}
|
||||
}
|
||||
|
||||
// expected successful
|
||||
table[GroupsService][SharePointService] = assert.NoError
|
||||
|
||||
for srv, ti := range table {
|
||||
for sub, expect := range ti {
|
||||
suite.Run(srv.String()+"-"+sub.String(), func() {
|
||||
err := ValidateServiceAndSubService(srv, sub)
|
||||
expect(suite.T(), err, clues.ToCore(err))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,7 @@ func (br backupReason) Category() path.CategoryType {
|
||||
}
|
||||
|
||||
func (br backupReason) SubtreePath() (path.Path, error) {
|
||||
return path.ServicePrefix(
|
||||
return path.BuildPrefix(
|
||||
br.tenant,
|
||||
br.resource,
|
||||
br.service,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user