Skip to content
This repository has been archived by the owner on Oct 11, 2024. It is now read-only.

Commit

Permalink
add export support to teamschats service layer
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanfkeepers committed Jan 25, 2024
1 parent 7ab1276 commit b9f7128
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 12 deletions.
4 changes: 4 additions & 0 deletions src/internal/m365/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/internal/m365/service/groups"
"github.com/alcionai/corso/src/internal/m365/service/onedrive"
"github.com/alcionai/corso/src/internal/m365/service/sharepoint"
"github.com/alcionai/corso/src/internal/m365/service/teamschats"
"github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/pkg/path"
)
Expand All @@ -30,6 +31,9 @@ func (ctrl *Controller) NewServiceHandler(

case path.ExchangeService:
return exchange.NewExchangeHandler(ctrl.AC, ctrl.resourceHandler), nil

case path.TeamsChatsService:
return teamschats.NewTeamsChatsHandler(ctrl.AC, ctrl.resourceHandler), nil
}

return nil, clues.New("unrecognized service").
Expand Down
13 changes: 7 additions & 6 deletions src/internal/m365/service/groups/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"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/data"
Expand Down Expand Up @@ -79,7 +80,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
)

p, err := path.Build("t", "pr", path.GroupsService, path.ChannelMessagesCategory, false, containerName)
assert.NoError(t, err, "build path")
require.NoError(t, err, clues.ToCore(err))

dcs := []data.RestoreCollection{
data.FetchRestoreCollection{
Expand All @@ -106,7 +107,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {
dcs,
stats,
fault.New(true))
assert.NoError(t, err, "export collections error")
require.NoError(t, err, clues.ToCore(err))
assert.Len(t, ecs, 1, "num of collections")

assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
Expand All @@ -117,7 +118,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_messages() {

for item := range ecs[0].Items(ctx) {
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
require.NoError(t, err, clues.ToCore(err))

// count up size for tests
size += len(b)
Expand Down Expand Up @@ -181,7 +182,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
false,
odConsts.SitesPathDir,
siteID)
assert.NoError(t, err, "build path")
require.NoError(t, err, clues.ToCore(err))

dcs := []data.RestoreCollection{
data.FetchRestoreCollection{
Expand Down Expand Up @@ -210,7 +211,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
dcs,
stats,
fault.New(true))
assert.NoError(t, err, "export collections error")
require.NoError(t, err, clues.ToCore(err))
assert.Len(t, ecs, 1, "num of collections")

assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")
Expand All @@ -222,7 +223,7 @@ func (suite *ExportUnitSuite) TestExportRestoreCollections_libraries() {
for item := range ecs[0].Items(ctx) {
// unwrap the body from stats reader
b, err := io.ReadAll(item.Body)
assert.NoError(t, err, clues.ToCore(err))
require.NoError(t, err, clues.ToCore(err))

size += len(b)
bitem := io.NopCloser(bytes.NewBuffer(b))
Expand Down
119 changes: 119 additions & 0 deletions src/internal/m365/service/teamschats/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package teamschats

import (
"context"

"github.com/alcionai/clues"

"github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/teamschats"
"github.com/alcionai/corso/src/internal/m365/resource"
"github.com/alcionai/corso/src/internal/operations/inject"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/metrics"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)

var _ inject.ServiceHandler = &teamsChatsHandler{}

Check failure on line 22 in src/internal/m365/service/teamschats/export.go

View workflow job for this annotation

GitHub Actions / Source-Code-Linting

cannot use &teamsChatsHandler{} (value of type *teamsChatsHandler) as "github.com/alcionai/corso/src/internal/operations/inject".ServiceHandler value in variable declaration: *teamsChatsHandler does not implement "github.com/alcionai/corso/src/internal/operations/inject".ServiceHandler (missing method ConsumeRestoreCollections) (typecheck)

func NewTeamsChatsHandler(
apiClient api.Client,
resourceGetter idname.GetResourceIDAndNamer,
) *teamsChatsHandler {
return &teamsChatsHandler{
baseTeamsChatsHandler: baseTeamsChatsHandler{},
apiClient: apiClient,
resourceGetter: resourceGetter,
}
}

// ========================================================================== //
// baseTeamsChatsHandler
// ========================================================================== //

// baseTeamsChatsHandler contains logic for tracking data and doing operations
// (e.x. export) that don't require contact with external M356 services.
type baseTeamsChatsHandler struct{}

func (h *baseTeamsChatsHandler) CacheItemInfo(v details.ItemInfo) {}

// ProduceExportCollections will create the export collections for the
// given restore collections.
func (h *baseTeamsChatsHandler) ProduceExportCollections(
ctx context.Context,
backupVersion int,
exportCfg control.ExportConfig,
dcs []data.RestoreCollection,
stats *metrics.ExportStats,
errs *fault.Bus,
) ([]export.Collectioner, error) {
var (
el = errs.Local()
ec = make([]export.Collectioner, 0, len(dcs))
)

for _, dc := range dcs {
category := dc.FullPath().Category()

switch category {
case path.ChatsCategory:
folders := dc.FullPath().Folders()
pth := path.Builder{}.Append(category.HumanString()).Append(folders...)

ec = append(
ec,
teamschats.NewExportCollection(
pth.String(),
[]data.RestoreCollection{dc},
backupVersion,
exportCfg,
stats))
default:
return nil, clues.NewWC(ctx, "data category not supported").
With("category", category)
}
}

return ec, el.Failure()
}

// ========================================================================== //
// teamschatsHandler
// ========================================================================== //

// teamsChatsHandler contains logic for handling data and performing operations
// (e.x. restore) regardless of whether they require contact with external M365
// services or not.
type teamsChatsHandler struct {
baseTeamsChatsHandler
apiClient api.Client
resourceGetter idname.GetResourceIDAndNamer
}

func (h *teamsChatsHandler) IsServiceEnabled(
ctx context.Context,
resourceID string,
) (bool, error) {
// TODO(ashmrtn): Move free function implementation to this function.
res, err := IsServiceEnabled(ctx, h.apiClient.Users(), resourceID)
return res, clues.Stack(err).OrNil()
}

func (h *teamsChatsHandler) PopulateProtectedResourceIDAndName(
ctx context.Context,
resourceID string, // Can be either ID or name.
ins idname.Cacher,
) (idname.Provider, error) {
if h.resourceGetter == nil {
return nil, clues.StackWC(ctx, resource.ErrNoResourceLookup)
}

pr, err := h.resourceGetter.GetResourceIDAndNameFrom(ctx, resourceID, ins)

return pr, clues.Wrap(err, "identifying resource owner").OrNil()
}
140 changes: 140 additions & 0 deletions src/internal/m365/service/teamschats/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package teamschats

import (
"bytes"
"context"
"io"
"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/data"
dataMock "github.com/alcionai/corso/src/internal/data/mock"
teamschatMock "github.com/alcionai/corso/src/internal/m365/service/teamschats/mock"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/export"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/metrics"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)

type ExportUnitSuite struct {
tester.Suite
}

func TestExportUnitSuite(t *testing.T) {
suite.Run(t, &ExportUnitSuite{Suite: tester.NewUnitSuite(t)})
}

type finD struct {
id string
key string
name string
err error
}

func (fd finD) FetchItemByName(ctx context.Context, name string) (data.Item, error) {
if fd.err != nil {
return nil, fd.err
}

if name == fd.id {
return &dataMock.Item{
ItemID: fd.id,
Reader: io.NopCloser(bytes.NewBufferString(`{"` + fd.key + `": "` + fd.name + `"}`)),
}, nil
}

return nil, assert.AnError
}

func (suite *ExportUnitSuite) TestExportRestoreCollections_chats() {
t := suite.T()

ctx, flush := tester.NewContext(t)
defer flush()

var (
category = path.ChatsCategory
itemID = "itemID"
dii = teamschatMock.ItemInfo()
content = `{"topic": "` + dii.TeamsChats.Chat.Topic + `"}`
body = io.NopCloser(bytes.NewBufferString(content))
exportCfg = control.ExportConfig{}
expectedPath = category.HumanString()
expectedItems = []export.Item{
{
ID: itemID,
Name: itemID + ".json",
// Body: body, not checked
},
}
)

p, err := path.BuildPrefix("t", "pr", path.TeamsChatsService, category)
require.NoError(t, err, clues.ToCore(err))

dcs := []data.RestoreCollection{
data.FetchRestoreCollection{
Collection: dataMock.Collection{
Path: p,
ItemData: []data.Item{
&dataMock.Item{
ItemID: itemID,
Reader: body,
},
},
},
FetchItemByNamer: finD{
id: itemID,
key: "id",
name: itemID,
},
},
}

stats := metrics.NewExportStats()

ecs, err := NewTeamsChatsHandler(api.Client{}, nil).
ProduceExportCollections(
ctx,
int(version.Backup),
exportCfg,
dcs,
stats,
fault.New(true))
require.NoError(t, err, clues.ToCore(err))
assert.Len(t, ecs, 1, "num of collections")

assert.Equal(t, expectedPath, ecs[0].BasePath(), "base dir")

fitems := []export.Item{}

size := 0

for item := range ecs[0].Items(ctx) {
b, err := io.ReadAll(item.Body)
require.NoError(t, err, clues.ToCore(err))

// count up size for tests
size += len(b)

// have to nil out body, otherwise assert fails due to
// pointer memory location differences
item.Body = nil
fitems = append(fitems, item)
}

assert.Equal(t, expectedItems, fitems, "items")

expectedStats := metrics.NewExportStats()
expectedStats.UpdateBytes(category, int64(size))
expectedStats.UpdateResourceCount(category)
assert.Equal(t, expectedStats.GetStats(), stats.GetStats(), "stats")
}
Loading

0 comments on commit b9f7128

Please sign in to comment.