diff --git a/features/concurrent-user-sharing.feature b/features/concurrent-user-sharing.feature new file mode 100644 index 0000000..3dd5d46 --- /dev/null +++ b/features/concurrent-user-sharing.feature @@ -0,0 +1,17 @@ +Feature: concurrent user sharing + As a user + I want to share their resource concurrently to different multiple users + + + Scenario: users make concurrent sharing to each other + Given user "admin" has logged in with password "admin" + And user "admin" has created a personal space with the alias "Admin Home" + And user "admin" has uploaded a file "testfile.txt" with content "concurrent sharing" in the home directory with the alias "testfile1" + When user "admin" shares a file "testfile.txt" with the following users concurrently + | users | + | marie | + | moss | + | richard | + | katherine | + | einstein | + Then the concurrent user sharing should have been successfull diff --git a/go.mod b/go.mod index bf8a6e0..7858fab 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/owncloud/cs3api-validator go 1.18 require ( - github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965 + github.com/cs3org/go-cs3apis v0.0.0-20230727093620-0f4399be4543 github.com/cs3org/reva/v2 v2.10.1-0.20221019091055-df0a189e218d github.com/cucumber/godog v0.12.2 github.com/cucumber/messages-go/v16 v16.0.1 diff --git a/go.sum b/go.sum index 2a01bdc..f9249fe 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965 h1:y4n2j68LLnvac+zw/al8MfPgO5aQiIwLmHM/JzYN8AM= github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20230727093620-0f4399be4543 h1:IFo6dj0XEOIA6i2baRWMC3vd+fAmuIUAVfSf77ZhoQg= +github.com/cs3org/go-cs3apis v0.0.0-20230727093620-0f4399be4543/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cs3org/reva/v2 v2.10.1-0.20221019091055-df0a189e218d h1:pYCrKLmcSF15jwWpougleGIeng+FehQf0LePxhR04kQ= github.com/cs3org/reva/v2 v2.10.1-0.20221019091055-df0a189e218d/go.mod h1:lq+LRpBDYU1vHUmJDeK7sGquREciO8GDj5/SYIibMPY= github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= diff --git a/scenario/featurecontext.go b/scenario/featurecontext.go index 35abd42..3357ed6 100644 --- a/scenario/featurecontext.go +++ b/scenario/featurecontext.go @@ -6,6 +6,7 @@ import ( "github.com/owncloud/cs3api-validator/steps/login" "github.com/owncloud/cs3api-validator/steps/publicshare" "github.com/owncloud/cs3api-validator/steps/resources" + "github.com/owncloud/cs3api-validator/steps/share" "github.com/owncloud/cs3api-validator/steps/spaces" ) @@ -17,6 +18,7 @@ type featureContext struct { *publicshare.PublicShareFeatureContext *resources.ResourcesFeatureContext *spaces.SpacesFeatureContext + *share.ShareTestFeatureContext } // newFeatureContext returns a new feature context for the scenario initialization @@ -32,6 +34,7 @@ func newFeatureContext(sc *godog.ScenarioContext) *featureContext { PublicShareFeatureContext: publicshare.NewPublicShareFeatureContext(fc, sc), ResourcesFeatureContext: resources.NewResourcesFeatureContext(fc, sc), SpacesFeatureContext: spaces.NewSpacesFeatureContext(fc, sc), + ShareTestFeatureContext: share.NewShareTestFeatureContext(fc, sc), } return uc } diff --git a/steps/share/context.go b/steps/share/context.go new file mode 100644 index 0000000..2861b67 --- /dev/null +++ b/steps/share/context.go @@ -0,0 +1,23 @@ +package share + +import ( + "github.com/cucumber/godog" + "github.com/owncloud/cs3api-validator/featurecontext" +) + +// ShareTestFeatureContext holds values which are used across test steps +type ShareTestFeatureContext struct { + *featurecontext.FeatureContext +} + +func NewShareTestFeatureContext(fc *featurecontext.FeatureContext, sc *godog.ScenarioContext) *ShareTestFeatureContext { + nsc := &ShareTestFeatureContext{FeatureContext: fc} + nsc.Register(sc) + return nsc +} + +func (f *ShareTestFeatureContext) Register(ctx *godog.ScenarioContext) { + // steps + ctx.Step(`^user "([^"]*)" shares a file "([^"]*)" with the following users concurrently$`, f.UserSharesAFileWithTheFollowingUsers) + ctx.Step(`^the concurrent user sharing should have been successfull$`, f.TheConcurrentUserSharingShouldHaveBeenSuccessfull) +} diff --git a/steps/share/steps.go b/steps/share/steps.go new file mode 100644 index 0000000..0904916 --- /dev/null +++ b/steps/share/steps.go @@ -0,0 +1,170 @@ +package share + +import ( + "fmt" + identityv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cucumber/godog" + "github.com/cucumber/messages-go/v16" + "github.com/owncloud/cs3api-validator/featurecontext" + "github.com/owncloud/cs3api-validator/helpers" + "sync" +) + +type CreateShareResult struct { + ResourceInformation *providerv1beta1.ResourceInfo + InformationOfSharee *identityv1beta1.UserId + Status *rpc.Status + Error error +} + +var concurentResults []*CreateShareResult + +func (f *ShareTestFeatureContext) UserSharesAFileWithTheFollowingUsers(shareer string, resourceName string, sharees *godog.Table) error { + ctx, err := f.GetAuthContext(shareer) + if err != nil { + return err + } + rows := sharees.Rows + if len(rows) == 0 { + return fmt.Errorf("empty gherkin table") + } + + if rows[0].Cells[0].Value != "users" { + return fmt.Errorf("the first line of the tables needs to be in the form | |") + } + + var rowsValues []*messages.PickleTableRow + rowsValues = append(rowsValues, rows[+1:]...) + + // to create a share we need information of each sharee + var collectedShareesInfos []* identityv1beta1.UserId + + for _, row := range rowsValues { + var sharee = row.Cells[0].Value + shareeInformations, err := f.Client.FindUsers( + ctx, + &identityv1beta1.FindUsersRequest{ + Filter: sharee, + }, + ) + if err != nil { + return err + } + if len(shareeInformations.GetUsers()) == 0 { + return fmt.Errorf("Could not find the user " + sharee) + } + collectedShareesInfos = append(collectedShareesInfos, shareeInformations.GetUsers()[0].GetId()) + } + + // also we need resource information to create a share for different users + var res featurecontext.ResourceAlias + res, ok := f.ResourceReferences["Admin Home"] + if !ok { + return fmt.Errorf("cannot find key %s in the remembered resource references map", "Admin Home") + } + if res.Info.Type != providerv1beta1.ResourceType_RESOURCE_TYPE_CONTAINER { + return fmt.Errorf("we cannot call list inside non-container resources") + } + resp, err := f.Client.ListContainer( + ctx, + &providerv1beta1.ListContainerRequest{ + Ref: res.Ref, + }, + ) + if err != nil { + return err + } + if resp.Status.Code != rpc.Code_CODE_OK { + return helpers.FormatError(resp.Status) + } + f.Response = resp + listContainerResponse, ok := f.Response.(*providerv1beta1.ListContainerResponse) + if !ok { + return fmt.Errorf("expected to receive a ListContainerResponse but got something different") + } + + + // we want specific file info to get shared + var resourceInfo *providerv1beta1.ResourceInfo + for _, resource := range listContainerResponse.GetInfos() { + if resource.Name == resourceName { + resourceInfo = resource + break + } + } + + if resourceInfo == nil { + return fmt.Errorf("Resource name " + resourceName + " could not be found in the container " + "Admin Home") + } + + // once resourceInformation and sharees information is known then we can make a concurrent share request to the server + var wg sync.WaitGroup + // store each result of the request during concurrent sharing + resultChannel := make(chan CreateShareResult, len(collectedShareesInfos)) + + for _, UserId := range collectedShareesInfos { + wg.Add(1) + go func(UserId *identityv1beta1.UserId) { + defer wg.Done() + createShareResponse, err := f.Client.CreateShare( + ctx, + &collaborationv1beta1.CreateShareRequest{ + ResourceInfo: resourceInfo, + Grant: &collaborationv1beta1.ShareGrant{ + Grantee: &providerv1beta1.Grantee{ + Type: 1, + Id: &providerv1beta1.Grantee_UserId{UserId: UserId}, + }, + Permissions: &collaborationv1beta1.SharePermissions{ + Permissions: &providerv1beta1.ResourcePermissions{ + AddGrant: true, + }, + }, + }, + }, + ) + + result := CreateShareResult{ + ResourceInformation: resourceInfo, + InformationOfSharee: UserId, + Status: createShareResponse.GetStatus(), + Error: err, + } + resultChannel <- result + + }(UserId) + } + wg.Wait() + close(resultChannel) + + for result := range resultChannel { + concurentResults = append(concurentResults, &CreateShareResult{ + ResourceInformation: result.ResourceInformation, + InformationOfSharee: result.InformationOfSharee, + Status: result.Status, + Error: result.Error, + }) + } + + return nil +} + +func (f *ShareTestFeatureContext) TheConcurrentUserSharingShouldHaveBeenSuccessfull() error { + //collect the result summary if there is any error while concurrent sharing + var isThereConcurrentError bool + var errorSummary string + for _, concurentResult := range concurentResults { + if concurentResult.Status.Code != rpc.Code_CODE_OK || concurentResult.Error != nil { + isThereConcurrentError = true + errorSummary = errorSummary + concurentResult.ResourceInformation.Name + " did not get shared to user with id " + concurentResult.InformationOfSharee.OpaqueId + "\n" + } + } + if isThereConcurrentError { + return fmt.Errorf(errorSummary) + } + return nil +} +