Repository: owncloud/ocis
Pull request #7358 (2023-09-26 09:16:21)

[tests-only][full-ci] backport refactoring the setresponse in given/then step in notification context
## Description
This pr backports  https://github.com/owncloud/ocis/pull/7324 from master to stable-4.0

## Related issue
https://github.com/owncloud/ocis/issues/7082

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7358)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7358&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7358&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7358&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7358&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7358&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=owncloud_ocis&pullRequest=7358)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7358&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7358&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=owncloud_ocis&pullRequest=7358&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=owncloud_ocis&pullRequest=7358&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7358&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=owncloud_ocis&pullRequest=7358&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6de73b12d2f..a21163f37bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,9 @@ The following sections list the changes for unreleased.
 
 ## Summary
 
+* Bugfix - Bump reva to 2.16.1: [#7350](https://github.com/owncloud/ocis/pull/7350)
 * Bugfix - Do not reset received share state to pending: [#7319](https://github.com/owncloud/ocis/issues/7319)
+* Bugfix - Check school number for duplicates before adding a school: [#7351](https://github.com/owncloud/ocis/pull/7351)
 * Bugfix - Disable username validation for keycloak example: [#7230](https://github.com/owncloud/ocis/pull/7230)
 * Bugfix - Actually pass PROXY_OIDC_SKIP_USER_INFO option to oidc client middleware: [#7220](https://github.com/owncloud/ocis/pull/7220)
 * Bugfix - Bring back the USERS_LDAP_USER_SCHEMA_ID variable: [#7312](https://github.com/owncloud/ocis/issues/7312)
@@ -14,6 +16,13 @@ The following sections list the changes for unreleased.
 
 ## Details
 
+* Bugfix - Bump reva to 2.16.1: [#7350](https://github.com/owncloud/ocis/pull/7350)
+
+  *   Bugfix [cs3org/reva#4194](https://github.com/cs3org/reva/pull/4194): Make appctx package compatible with go v1.21
+  *   Bugfix [cs3org/reva#4214](https://github.com/cs3org/reva/pull/4214): Always pass adjusted default nats options
+
+   https://github.com/owncloud/ocis/pull/7350
+
 * Bugfix - Do not reset received share state to pending: [#7319](https://github.com/owncloud/ocis/issues/7319)
 
    We fixed a problem where the states of received shares were reset to PENDING in the "ocis migrate
@@ -21,6 +30,13 @@ The following sections list the changes for unreleased.
 
    https://github.com/owncloud/ocis/issues/7319
 
+* Bugfix - Check school number for duplicates before adding a school: [#7351](https://github.com/owncloud/ocis/pull/7351)
+
+   We fixed an issue that allowed to create two schools with the same school number
+
+   https://github.com/owncloud/enterprise/issues/6051
+   https://github.com/owncloud/ocis/pull/7351
+
 * Bugfix - Disable username validation for keycloak example: [#7230](https://github.com/owncloud/ocis/pull/7230)
 
    Set 'GRAPH_USERNAME_MATCH' to 'none'. To accept any username that is also valid for keycloak.
diff --git a/changelog/unreleased/bump-reva.md b/changelog/unreleased/bump-reva.md
new file mode 100644
index 00000000000..c5290bf75a2
--- /dev/null
+++ b/changelog/unreleased/bump-reva.md
@@ -0,0 +1,6 @@
+Bugfix: Bump reva to 2.16.1
+
+*   Bugfix [cs3org/reva#4194](https://github.com/cs3org/reva/pull/4194): Make appctx package compatible with go v1.21
+*   Bugfix [cs3org/reva#4214](https://github.com/cs3org/reva/pull/4214): Always pass adjusted default nats options
+
+https://github.com/owncloud/ocis/pull/7350
\ No newline at end of file
diff --git a/changelog/unreleased/fix-graph-education-createschool.md b/changelog/unreleased/fix-graph-education-createschool.md
new file mode 100644
index 00000000000..4d867a4556f
--- /dev/null
+++ b/changelog/unreleased/fix-graph-education-createschool.md
@@ -0,0 +1,6 @@
+Bugfix: Check school number for duplicates before adding a school
+
+We fixed an issue that allowed to create two schools with the same school number
+
+https://github.com/owncloud/ocis/pull/7351
+https://github.com/owncloud/enterprise/issues/6051
diff --git a/go.mod b/go.mod
index 502dac70a26..fa409ff2fa6 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/coreos/go-oidc v2.2.1+incompatible
 	github.com/coreos/go-oidc/v3 v3.6.0
 	github.com/cs3org/go-cs3apis v0.0.0-20230516150832-730ac860c71d
-	github.com/cs3org/reva/v2 v2.16.0
+	github.com/cs3org/reva/v2 v2.16.1
 	github.com/disintegration/imaging v1.6.2
 	github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
 	github.com/egirna/icap-client v0.1.1
diff --git a/go.sum b/go.sum
index 0655641fa72..d308d3f5574 100644
--- a/go.sum
+++ b/go.sum
@@ -864,10 +864,8 @@ github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo
 github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
 github.com/crewjam/saml v0.4.13 h1:TYHggH/hwP7eArqiXSJUvtOPNzQDyQ7vwmwEqlFWhMc=
 github.com/crewjam/saml v0.4.13/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
-github.com/cs3org/reva/v2 v2.15.1-0.20230816081257-e3a2be91bc4f h1:s0sBJbIB8atyhujVx/OaadujuRHer8ODPpWxyGWfw/s=
-github.com/cs3org/reva/v2 v2.15.1-0.20230816081257-e3a2be91bc4f/go.mod h1:6GyXffmxluCqQxXaYuVC2Dg10gj0QW199iVlxV0EAJg=
-github.com/cs3org/reva/v2 v2.16.0 h1:XBFoGhzKrcDqqRvYdicOpDOpQVIsRqALmEt8X0N+wm0=
-github.com/cs3org/reva/v2 v2.16.0/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
+github.com/cs3org/reva/v2 v2.16.1 h1:mJA3Vg5nrrOXx1CcGUKrZ+gBR6bhRSqkVY2pqD3G55Q=
+github.com/cs3org/reva/v2 v2.16.1/go.mod h1:RvhuweTFqzezjUFU0SIdTXakrEx9vJlMvQ7znPXSP1g=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
diff --git a/services/graph/pkg/identity/ldap_education_school.go b/services/graph/pkg/identity/ldap_education_school.go
index 6274c2e8f0b..fe02f4b262b 100644
--- a/services/graph/pkg/identity/ldap_education_school.go
+++ b/services/graph/pkg/identity/ldap_education_school.go
@@ -49,7 +49,11 @@ const (
 
 const ldapDateFormat = "20060102150405Z0700"
 
-var errNotSet = errors.New("Attribute not set")
+var (
+	errNotSet             = errors.New("Attribute not set")
+	errSchoolNameExists   = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
+	errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
+)
 
 func defaultEducationConfig() educationConfig {
 	return educationConfig{
@@ -116,7 +120,18 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
 		return nil, ErrReadOnly
 	}
 
-	// Here we should verify that the school number is not already used
+	// Check that the school number is not already used
+	_, err := i.getSchoolByNumber(school.GetSchoolNumber())
+	switch err {
+	case nil:
+		logger.Debug().Err(errSchoolNumberExists).Str("schoolNumber", school.GetSchoolNumber()).Msg("duplicate school number")
+		return nil, errSchoolNumberExists
+	case ErrNotFound:
+		break
+	default:
+		logger.Error().Err(err).Str("schoolNumber", school.GetSchoolNumber()).Msg("error looking up school by number")
+		return nil, errorcode.New(errorcode.GeneralException, "error looking up school by number")
+	}
 
 	attributeTypeAndValue := ldap.AttributeTypeAndValue{
 		Type:  i.educationConfig.schoolAttributeMap.displayName,
@@ -141,7 +156,7 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
 		logger.Debug().Err(err).Msg("error adding school")
 		if errors.As(err, &lerr) {
 			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
-				err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
+				err = errSchoolNameExists
 			}
 		}
 		return nil, err
@@ -156,7 +171,7 @@ func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.Educ
 }
 
 // UpdateEducationSchoolOperation contains the logic for which update operation to apply to a school
-func (i *LDAP) UpdateEducationSchoolOperation(
+func (i *LDAP) updateEducationSchoolOperation(
 	schoolUpdate libregraph.EducationSchool,
 	currentSchool libregraph.EducationSchool,
 ) schoolUpdateOperation {
@@ -216,7 +231,7 @@ func (i *LDAP) updateDisplayName(ctx context.Context, dn string, providedDisplay
 		logger.Debug().Err(err).Msg("error updating school name")
 		if errors.As(err, &lerr) {
 			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
-				err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
+				err = errSchoolNameExists
 			}
 		}
 		return err
@@ -233,11 +248,9 @@ func (i *LDAP) updateSchoolProperties(ctx context.Context, dn string, currentSch
 	mr := ldap.NewModifyRequest(dn, nil)
 	if updatedSchoolNumber, ok := updatedSchool.GetSchoolNumberOk(); ok {
 		if *updatedSchoolNumber != "" && currentSchool.GetSchoolNumber() != *updatedSchoolNumber {
-			_, err := i.getSchoolByNumberOrID(*updatedSchoolNumber)
+			_, err := i.getSchoolByNumber(*updatedSchoolNumber)
 			if err == nil {
-				errmsg := fmt.Sprintf("school number '%s' already exists", *updatedSchoolNumber)
-				err = fmt.Errorf(errmsg)
-				return err
+				return errSchoolNumberExists
 			}
 			mr.Replace(i.educationConfig.schoolAttributeMap.schoolNumber, []string{*updatedSchoolNumber})
 		}
@@ -255,13 +268,7 @@ func (i *LDAP) updateSchoolProperties(ctx context.Context, dn string, currentSch
 	}
 
 	if err := i.conn.Modify(mr); err != nil {
-		var lerr *ldap.Error
 		logger.Debug().Err(err).Msg("error updating school number")
-		if errors.As(err, &lerr) {
-			if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
-				err = errorcode.New(errorcode.NameAlreadyExists, lerr.Error())
-			}
-		}
 		return err
 	}
 
@@ -282,7 +289,7 @@ func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrID string, sch
 	}
 
 	currentSchool := i.createSchoolModelFromLDAP(e)
-	switch i.UpdateEducationSchoolOperation(school, *currentSchool) {
+	switch i.updateEducationSchoolOperation(school, *currentSchool) {
 	case tooManyValues:
 		return nil, fmt.Errorf("school name and school number cannot be updated in the same request")
 	case schoolUnchanged:
@@ -635,17 +642,12 @@ func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberO
 }
 
 func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) {
-	attrs := []string{
-		i.educationConfig.schoolAttributeMap.displayName,
-		i.educationConfig.schoolAttributeMap.id,
-		i.educationConfig.schoolAttributeMap.schoolNumber,
-	}
 	filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
 
 	if i.educationConfig.schoolFilter != "" {
 		filter = fmt.Sprintf("(&%s(%s))", filter, i.educationConfig.schoolFilter)
 	}
-	return i.getEntryByDN(dn, attrs, filter)
+	return i.getEntryByDN(dn, i.getEducationSchoolAttrTypes(), filter)
 }
 
 func (i *LDAP) getSchoolByNumberOrID(numberOrID string) (*ldap.Entry, error) {
@@ -660,6 +662,16 @@ func (i *LDAP) getSchoolByNumberOrID(numberOrID string) (*ldap.Entry, error) {
 	return i.getSchoolByFilter(filter)
 }
 
+func (i *LDAP) getSchoolByNumber(schoolNumber string) (*ldap.Entry, error) {
+	schoolNumber = ldap.EscapeFilter(schoolNumber)
+	filter := fmt.Sprintf(
+		"(%s=%s)",
+		i.educationConfig.schoolAttributeMap.schoolNumber,
+		schoolNumber,
+	)
+	return i.getSchoolByFilter(filter)
+}
+
 func (i *LDAP) getSchoolByFilter(filter string) (*ldap.Entry, error) {
 	filter = fmt.Sprintf("(&%s(objectClass=%s)%s)",
 		i.educationConfig.schoolFilter,
diff --git a/services/graph/pkg/service/v0/educationschools.go b/services/graph/pkg/service/v0/educationschools.go
index eb7b10d7e7b..8a176ef2057 100644
--- a/services/graph/pkg/service/v0/educationschools.go
+++ b/services/graph/pkg/service/v0/educationschools.go
@@ -148,7 +148,7 @@ func (g Graph) PatchEducationSchool(w http.ResponseWriter, r *http.Request) {
 
 	if school, err = g.identityEducationBackend.UpdateEducationSchool(r.Context(), schoolID, *school); err != nil {
 		logger.Debug().Err(err).Interface("school", school).Msg("could not update school: backend error")
-		errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
+		errorcode.RenderError(w, r, err)
 		return
 	}
 
diff --git a/services/graph/pkg/service/v0/errorcode/errorcode.go b/services/graph/pkg/service/v0/errorcode/errorcode.go
index e15289f327f..f235759c957 100644
--- a/services/graph/pkg/service/v0/errorcode/errorcode.go
+++ b/services/graph/pkg/service/v0/errorcode/errorcode.go
@@ -1,3 +1,4 @@
+// Package errorcode allows to deal with graph error codes
 package errorcode
 
 import (
@@ -13,6 +14,7 @@ import (
 // ErrorCode defines code as used in MS Graph - see https://docs.microsoft.com/en-us/graph/errors?context=graph%2Fapi%2F1.0&view=graph-rest-1.0
 type ErrorCode int
 
+// Error defines a custom error struct, containing and MS Graph error code an a textual error message
 type Error struct {
 	errorCode ErrorCode
 	msg       string
@@ -75,6 +77,7 @@ var errorCodes = [...]string{
 	"preconditionFailed",
 }
 
+// New constructs a new errorcode.Error
 func New(e ErrorCode, msg string) Error {
 	return Error{
 		errorCode: e,
@@ -82,7 +85,7 @@ func New(e ErrorCode, msg string) Error {
 	}
 }
 
-// Render writes an Graph ErrorObject to the response writer
+// Render writes an Graph ErrorCode	object to the response writer
 func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) {
 	innererror := map[string]interface{}{
 		"date": time.Now().UTC().Format(time.RFC3339),
@@ -100,6 +103,7 @@ func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, ms
 	render.JSON(w, r, resp)
 }
 
+// Render writes an Graph Error object to the response writer
 func (e Error) Render(w http.ResponseWriter, r *http.Request) {
 	var status int
 	switch e.errorCode {
@@ -115,12 +119,18 @@ func (e Error) Render(w http.ResponseWriter, r *http.Request) {
 	e.errorCode.Render(w, r, status, e.msg)
 }
 
+// String returns the string corresponding to the ErrorCode
 func (e ErrorCode) String() string {
 	return errorCodes[e]
 }
 
+// Error return the concatenation of the error string and optinal message
 func (e Error) Error() string {
-	return errorCodes[e.errorCode]
+	errString := errorCodes[e.errorCode]
+	if e.msg != "" {
+		errString += ": " + e.msg
+	}
+	return errString
 }
 
 // RenderError render the Graph Error based on a code or default one
diff --git a/vendor/github.com/cs3org/reva/v2/pkg/appctx/ctxmap.go b/vendor/github.com/cs3org/reva/v2/pkg/appctx/ctxmap.go
index a7e10eaa8af..1acdec7bd99 100644
--- a/vendor/github.com/cs3org/reva/v2/pkg/appctx/ctxmap.go
+++ b/vendor/github.com/cs3org/reva/v2/pkg/appctx/ctxmap.go
@@ -41,20 +41,24 @@ func GetKeyValuesFromCtx(ctx context.Context) map[interface{}]interface{} {
 }
 
 func getKeyValue(ctx interface{}, m map[interface{}]interface{}) {
-	ctxVals := reflect.ValueOf(ctx).Elem()
-	ctxType := reflect.TypeOf(ctx).Elem()
+	// This is a dirty hack to run with go 1.21.x
+	reflectCtxValues := reflect.ValueOf(ctx)
+	if reflectCtxValues.Kind() != reflect.Struct {
+		ctxVals := reflectCtxValues.Elem()
+		ctxType := reflect.TypeOf(ctx).Elem()
 
-	if ctxType.Kind() == reflect.Struct {
-		for i := 0; i < ctxVals.NumField(); i++ {
-			currField, currIf := extractField(ctxVals, ctxType, i)
-			switch currField {
-			case "Context":
-				getKeyValue(currIf, m)
-			case "key":
-				nextField, nextIf := extractField(ctxVals, ctxType, i+1)
-				if nextField == "val" {
-					m[currIf] = nextIf
-					i++
+		if ctxType.Kind() == reflect.Struct {
+			for i := 0; i < ctxVals.NumField(); i++ {
+				currField, currIf := extractField(ctxVals, ctxType, i)
+				switch currField {
+				case "Context":
+					getKeyValue(currIf, m)
+				case "key":
+					nextField, nextIf := extractField(ctxVals, ctxType, i+1)
+					if nextField == "val" {
+						m[currIf] = nextIf
+						i++
+					}
 				}
 			}
 		}
diff --git a/vendor/github.com/cs3org/reva/v2/pkg/store/store.go b/vendor/github.com/cs3org/reva/v2/pkg/store/store.go
index 79a84a065b6..68f71e3fac3 100644
--- a/vendor/github.com/cs3org/reva/v2/pkg/store/store.go
+++ b/vendor/github.com/cs3org/reva/v2/pkg/store/store.go
@@ -119,9 +119,11 @@ func Create(opts ...microstore.Option) microstore.Store {
 		// TODO nats needs a DefaultTTL option as it does not support per Write TTL ...
 		// FIXME nats has restrictions on the key, we cannot use slashes AFAICT
 		// host, port, clusterid
+		natsOptions := nats.GetDefaultOptions()
+		natsOptions.Name = "TODO" // we can pass in the service name to allow identifying the client, but that requires adding a custom context option
 		return natsjs.NewStore(
 			append(opts,
-				natsjs.NatsOptions(nats.Options{Name: "TODO"}),
+				natsjs.NatsOptions(natsOptions), // always pass in properly initialized default nats options
 				natsjs.DefaultTTL(ttl))...,
 		) // TODO test with ocis nats
 	case TypeMemory, "mem", "": // allow existing short form and use as default
diff --git a/vendor/modules.txt b/vendor/modules.txt
index b18a56afc87..0492366f437 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -354,7 +354,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
 github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
 github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
 github.com/cs3org/go-cs3apis/cs3/types/v1beta1
-# github.com/cs3org/reva/v2 v2.16.0
+# github.com/cs3org/reva/v2 v2.16.1
 ## explicit; go 1.20
 github.com/cs3org/reva/v2/cmd/revad/internal/grace
 github.com/cs3org/reva/v2/cmd/revad/runtime


Test patch:
diff --git a/services/graph/pkg/identity/ldap_education_class_test.go b/services/graph/pkg/identity/ldap_education_class_test.go
index b8f481b142d..b6f34cc8a3a 100644
--- a/services/graph/pkg/identity/ldap_education_class_test.go
+++ b/services/graph/pkg/identity/ldap_education_class_test.go
@@ -72,9 +72,7 @@ func TestGetEducationClasses(t *testing.T) {
 	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetEducationClasses(context.Background())
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	lm = &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
@@ -156,7 +154,7 @@ func TestGetEducationClass(t *testing.T) {
 
 		if tt.expectedItemNotFound {
 			assert.NotNil(t, err)
-			assert.Equal(t, "itemNotFound", err.Error())
+			assert.Equal(t, "itemNotFound: not found", err.Error())
 		} else {
 			assert.Nil(t, err)
 			assert.Equal(t, "Math", class.GetDisplayName())
@@ -228,7 +226,7 @@ func TestDeleteEducationClass(t *testing.T) {
 		if tt.expectedItemNotFound {
 			lm.AssertNumberOfCalls(t, "Del", 0)
 			assert.NotNil(t, err)
-			assert.Equal(t, "itemNotFound", err.Error())
+			assert.Equal(t, "itemNotFound: not found", err.Error())
 		} else {
 			assert.Nil(t, err)
 		}
@@ -301,7 +299,7 @@ func TestGetEducationClassMembers(t *testing.T) {
 		if tt.expectedItemNotFound {
 			lm.AssertNumberOfCalls(t, "Search", 1)
 			assert.NotNil(t, err)
-			assert.Equal(t, "itemNotFound", err.Error())
+			assert.Equal(t, "itemNotFound: not found", err.Error())
 		} else {
 			lm.AssertNumberOfCalls(t, "Search", 2)
 			assert.Nil(t, err)
diff --git a/services/graph/pkg/identity/ldap_education_school_test.go b/services/graph/pkg/identity/ldap_education_school_test.go
index d0e06353b70..520cdc5c900 100644
--- a/services/graph/pkg/identity/ldap_education_school_test.go
+++ b/services/graph/pkg/identity/ldap_education_school_test.go
@@ -2,6 +2,7 @@ package identity
 
 import (
 	"context"
+	"errors"
 	"testing"
 	"time"
 
@@ -9,6 +10,7 @@ import (
 	libregraph "github.com/owncloud/libre-graph-api-go"
 	"github.com/owncloud/ocis/v2/services/graph/mocks"
 	"github.com/owncloud/ocis/v2/services/graph/pkg/config"
+	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -67,45 +69,135 @@ var (
 )
 
 func TestCreateEducationSchool(t *testing.T) {
-	lm := &mocks.Client{}
-
-	ldapSchoolAddRequestMatcher := func(ar *ldap.AddRequest) bool {
-		if ar.DN != "ou=Test School," {
-			return false
-		}
-		for _, attr := range ar.Attributes {
-			if attr.Type == "ocEducationSchoolTerminationTimestamp" {
+	tests := []struct {
+		name          string
+		schoolNumber  string
+		schoolName    string
+		expectedError error
+	}{
+		{
+			name:          "Create a Education School succeeds",
+			schoolNumber:  "0123",
+			schoolName:    "Test School",
+			expectedError: nil,
+		}, {
+			name:          "Create a Education School with a duplicated Schoolnumber fails with an error",
+			schoolNumber:  "0666",
+			schoolName:    "Test School",
+			expectedError: errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present"),
+		}, {
+			name:          "Create a Education School with a duplicated Name fails with an error",
+			schoolNumber:  "0123",
+			schoolName:    "Existing Test School",
+			expectedError: errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present"),
+		}, {
+			name:          "Create a Education School fails, when there is a backend error",
+			schoolNumber:  "1111",
+			schoolName:    "Test School",
+			expectedError: errorcode.New(errorcode.GeneralException, "error looking up school by number"),
+		},
+	}
+	for _, tt := range tests {
+		lm := &mocks.Client{}
+		ldapSchoolGoodAddRequestMatcher := func(ar *ldap.AddRequest) bool {
+			if ar.DN != "ou=Test School," {
 				return false
 			}
+			for _, attr := range ar.Attributes {
+				if attr.Type == "ocEducationSchoolTerminationTimestamp" {
+					return false
+				}
+			}
+			return true
 		}
-		return true
-	}
-
-	lm.On("Add", mock.MatchedBy(ldapSchoolAddRequestMatcher)).Return(nil)
+		lm.On("Add", mock.MatchedBy(ldapSchoolGoodAddRequestMatcher)).Return(nil)
 
-	lm.On("Search", mock.Anything).
-		Return(
-			&ldap.SearchResult{
-				Entries: []*ldap.Entry{schoolEntry},
-			},
-			nil)
+		ldapExistingSchoolAddRequestMatcher := func(ar *ldap.AddRequest) bool {
+			if ar.DN == "ou=Existing Test School," {
+				return true
+			}
+			return false
+		}
+		lm.On("Add", mock.MatchedBy(ldapExistingSchoolAddRequestMatcher)).Return(ldap.NewError(ldap.LDAPResultEntryAlreadyExists, errors.New("")))
 
-	b, err := getMockedBackend(lm, eduConfig, &logger)
-	assert.Nil(t, err)
-	assert.NotEqual(t, "", b.educationConfig.schoolObjectClass)
-	school := libregraph.NewEducationSchool()
-	school.SetDisplayName("Test School")
-	school.SetSchoolNumber("0123")
-	school.SetId("abcd-defg")
-	res_school, err := b.CreateEducationSchool(context.Background(), *school)
-	lm.AssertNumberOfCalls(t, "Add", 1)
-	lm.AssertNumberOfCalls(t, "Search", 1)
-	assert.Nil(t, err)
-	assert.NotNil(t, res_school)
-	assert.Equal(t, res_school.GetDisplayName(), school.GetDisplayName())
-	assert.Equal(t, res_school.GetId(), school.GetId())
-	assert.Equal(t, res_school.GetSchoolNumber(), school.GetSchoolNumber())
-	assert.False(t, res_school.HasTerminationDate())
+		schoolNumberSearchRequest := &ldap.SearchRequest{
+			BaseDN:     "",
+			Scope:      2,
+			SizeLimit:  1,
+			Filter:     "(&(objectClass=ocEducationSchool)(ocEducationSchoolNumber=0123))",
+			Attributes: []string{"ou", "owncloudUUID", "ocEducationSchoolNumber", "ocEducationSchoolTerminationTimestamp"},
+			Controls:   []ldap.Control(nil),
+		}
+		lm.On("Search", schoolNumberSearchRequest).
+			Return(
+				&ldap.SearchResult{
+					Entries: []*ldap.Entry{},
+				},
+				nil)
+		existingSchoolNumberSearchRequest := &ldap.SearchRequest{
+			BaseDN:     "",
+			Scope:      2,
+			SizeLimit:  1,
+			Filter:     "(&(objectClass=ocEducationSchool)(ocEducationSchoolNumber=0666))",
+			Attributes: []string{"ou", "owncloudUUID", "ocEducationSchoolNumber", "ocEducationSchoolTerminationTimestamp"},
+			Controls:   []ldap.Control(nil),
+		}
+		lm.On("Search", existingSchoolNumberSearchRequest).
+			Return(
+				&ldap.SearchResult{
+					Entries: []*ldap.Entry{schoolEntry},
+				},
+				nil)
+		schoolNumberSearchRequestError := &ldap.SearchRequest{
+			BaseDN:     "",
+			Scope:      2,
+			SizeLimit:  1,
+			Filter:     "(&(objectClass=ocEducationSchool)(ocEducationSchoolNumber=1111))",
+			Attributes: []string{"ou", "owncloudUUID", "ocEducationSchoolNumber", "ocEducationSchoolTerminationTimestamp"},
+			Controls:   []ldap.Control(nil),
+		}
+		lm.On("Search", schoolNumberSearchRequestError).
+			Return(
+				&ldap.SearchResult{
+					Entries: []*ldap.Entry{},
+				},
+				ldap.NewError(ldap.LDAPResultOther, errors.New("some error")))
+		schoolLookupAfterCreate := &ldap.SearchRequest{
+			BaseDN:     "ou=Test School,",
+			Scope:      0,
+			SizeLimit:  1,
+			Filter:     "(objectClass=ocEducationSchool)",
+			Attributes: []string{"ou", "owncloudUUID", "ocEducationSchoolNumber", "ocEducationSchoolTerminationTimestamp"},
+			Controls:   []ldap.Control(nil),
+		}
+		lm.On("Search", schoolLookupAfterCreate).
+			Return(
+				&ldap.SearchResult{
+					Entries: []*ldap.Entry{schoolEntry},
+				},
+				nil)
+		b, err := getMockedBackend(lm, eduConfig, &logger)
+		assert.Nil(t, err)
+		assert.NotEqual(t, "", b.educationConfig.schoolObjectClass)
+
+		school := libregraph.NewEducationSchool()
+		school.SetDisplayName(tt.schoolName)
+		school.SetSchoolNumber(tt.schoolNumber)
+		school.SetId("abcd-defg")
+		res_school, err := b.CreateEducationSchool(context.Background(), *school)
+		if tt.expectedError == nil {
+			assert.Nil(t, err)
+			lm.AssertNumberOfCalls(t, "Add", 1)
+			assert.NotNil(t, res_school)
+			assert.Equal(t, res_school.GetDisplayName(), school.GetDisplayName())
+			assert.Equal(t, res_school.GetId(), school.GetId())
+			assert.Equal(t, res_school.GetSchoolNumber(), school.GetSchoolNumber())
+			assert.False(t, res_school.HasTerminationDate())
+		} else {
+			assert.Equal(t, err, tt.expectedError)
+			assert.Nil(t, res_school)
+		}
+	}
 }
 
 func TestUpdateEducationSchoolTerminationDate(t *testing.T) {
@@ -218,7 +310,7 @@ func TestUpdateEducationSchoolOperation(t *testing.T) {
 			SchoolNumber: &tt.schoolNumber,
 		}
 
-		operation := b.UpdateEducationSchoolOperation(schoolUpdate, currentSchool)
+		operation := b.updateEducationSchoolOperation(schoolUpdate, currentSchool)
 		assert.Equal(t, tt.expectedOperation, operation)
 	}
 }
@@ -285,7 +377,7 @@ func TestDeleteEducationSchool(t *testing.T) {
 		if tt.expectedItemNotFound {
 			lm.AssertNumberOfCalls(t, "Del", 0)
 			assert.NotNil(t, err)
-			assert.Equal(t, "itemNotFound", err.Error())
+			assert.Equal(t, "itemNotFound: not found", err.Error())
 		} else {
 			assert.Nil(t, err)
 		}
@@ -349,7 +441,7 @@ func TestGetEducationSchool(t *testing.T) {
 
 		if tt.expectedItemNotFound {
 			assert.NotNil(t, err)
-			assert.Equal(t, "itemNotFound", err.Error())
+			assert.Equal(t, "itemNotFound: not found", err.Error())
 		} else {
 			assert.Nil(t, err)
 			assert.Equal(t, "Test School", school.GetDisplayName())
@@ -502,7 +594,7 @@ func TestRemoveMemberFromEducationSchool(t *testing.T) {
 	err = b.RemoveUserFromEducationSchool(context.Background(), "abcd-defg", "does-not-exist")
 	lm.AssertNumberOfCalls(t, "Search", 2)
 	assert.NotNil(t, err)
-	assert.Equal(t, "itemNotFound", err.Error())
+	assert.Equal(t, "itemNotFound: not found", err.Error())
 	err = b.RemoveUserFromEducationSchool(context.Background(), "abcd-defg", "abcd-defg")
 	lm.AssertNumberOfCalls(t, "Search", 4)
 	lm.AssertNumberOfCalls(t, "Modify", 1)
@@ -613,7 +705,7 @@ func TestRemoveClassFromEducationSchool(t *testing.T) {
 	err = b.RemoveClassFromEducationSchool(context.Background(), "abcd-defg", "does-not-exist")
 	lm.AssertNumberOfCalls(t, "Search", 2)
 	assert.NotNil(t, err)
-	assert.Equal(t, "itemNotFound", err.Error())
+	assert.Equal(t, "itemNotFound: not found", err.Error())
 	err = b.RemoveClassFromEducationSchool(context.Background(), "abcd-defg", "abcd-defg")
 	lm.AssertNumberOfCalls(t, "Search", 4)
 	lm.AssertNumberOfCalls(t, "Modify", 1)
diff --git a/services/graph/pkg/identity/ldap_education_user_test.go b/services/graph/pkg/identity/ldap_education_user_test.go
index 8b0e9fff53f..763f27b4f37 100644
--- a/services/graph/pkg/identity/ldap_education_user_test.go
+++ b/services/graph/pkg/identity/ldap_education_user_test.go
@@ -139,7 +139,7 @@ func TestDeleteEducationUser(t *testing.T) {
 	lm.AssertNumberOfCalls(t, "Search", 2)
 	lm.AssertNumberOfCalls(t, "Del", 1)
 	assert.NotNil(t, err)
-	assert.Equal(t, "itemNotFound", err.Error())
+	assert.Equal(t, "itemNotFound: not found", err.Error())
 }
 
 func TestGetEducationUser(t *testing.T) {
@@ -157,7 +157,7 @@ func TestGetEducationUser(t *testing.T) {
 	_, err = b.GetEducationUser(context.Background(), "xxxx-xxxx")
 	lm.AssertNumberOfCalls(t, "Search", 2)
 	assert.NotNil(t, err)
-	assert.Equal(t, "itemNotFound", err.Error())
+	assert.Equal(t, "itemNotFound: not found", err.Error())
 }
 
 func TestGetEducationUsers(t *testing.T) {
diff --git a/services/graph/pkg/identity/ldap_group_test.go b/services/graph/pkg/identity/ldap_group_test.go
index e3613967b87..79431d4760f 100644
--- a/services/graph/pkg/identity/ldap_group_test.go
+++ b/services/graph/pkg/identity/ldap_group_test.go
@@ -59,34 +59,22 @@ func TestGetGroup(t *testing.T) {
 
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetGroup(context.Background(), "group", nil)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 	_, err = b.GetGroup(context.Background(), "group", queryParamExpand)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 	_, err = b.GetGroup(context.Background(), "group", queryParamSelect)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	// Mock an empty Search Result
 	lm = &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	_, err = b.GetGroup(context.Background(), "group", nil)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 	_, err = b.GetGroup(context.Background(), "group", queryParamExpand)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 	_, err = b.GetGroup(context.Background(), "group", queryParamSelect)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	// Mock an invalid Search Result
 	lm = &mocks.Client{}
@@ -95,17 +83,11 @@ func TestGetGroup(t *testing.T) {
 	}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	g, err := b.GetGroup(context.Background(), "group", nil)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 	g, err = b.GetGroup(context.Background(), "group", queryParamExpand)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 	g, err = b.GetGroup(context.Background(), "group", queryParamSelect)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	// Mock a valid	Search Result
 	lm = &mocks.Client{}
@@ -240,9 +222,7 @@ func TestGetGroups(t *testing.T) {
 	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err := b.GetGroups(context.Background(), url.Values{})
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	lm = &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
diff --git a/services/graph/pkg/identity/ldap_test.go b/services/graph/pkg/identity/ldap_test.go
index fcd5a551ae9..908862f8197 100644
--- a/services/graph/pkg/identity/ldap_test.go
+++ b/services/graph/pkg/identity/ldap_test.go
@@ -205,14 +205,10 @@ func TestGetUser(t *testing.T) {
 	}
 
 	_, err = b.GetUser(context.Background(), "fred", odataReqDefault)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	_, err = b.GetUser(context.Background(), "fred", odataReqExpand)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	// Mock an empty Search Result
 	lm = &mocks.Client{}
@@ -221,14 +217,10 @@ func TestGetUser(t *testing.T) {
 			&ldap.SearchResult{}, nil)
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	_, err = b.GetUser(context.Background(), "fred", odataReqDefault)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	_, err = b.GetUser(context.Background(), "fred", odataReqExpand)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 
 	// Mock a valid Search Result
 	lm = &mocks.Client{}
@@ -265,9 +257,7 @@ func TestGetUser(t *testing.T) {
 
 	b, _ = getMockedBackend(lm, lconfig, &logger)
 	_, err = b.GetUser(context.Background(), "invalid", nil)
-	if err == nil || err.Error() != "itemNotFound" {
-		t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "itemNotFound:")
 }
 
 func TestGetUsers(t *testing.T) {
@@ -283,9 +273,7 @@ func TestGetUsers(t *testing.T) {
 
 	b, _ := getMockedBackend(lm, lconfig, &logger)
 	_, err = b.GetUsers(context.Background(), odataReqDefault)
-	if err == nil || err.Error() != "generalException" {
-		t.Errorf("Expected 'generalException' got '%s'", err.Error())
-	}
+	assert.ErrorContains(t, err, "generalException:")
 
 	lm = &mocks.Client{}
 	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
diff --git a/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature b/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature
new file mode 100644
index 00000000000..6a6f9c90684
--- /dev/null
+++ b/tests/acceptance/features/apiSpacesDavOperation/copyByFileId.feature
@@ -0,0 +1,70 @@
+Feature: copying file using file id
+  As a user
+  I want to copy the file using file id
+  So that I can manage my resource
+
+  Background:
+    Given using spaces DAV path
+    And user "Alice" has been created with default attributes and without skeleton files
+
+
+  Scenario Outline: copy a file into a folder in personal space
+    Given user "Alice" has created folder "/folder"
+    And user "Alice" has uploaded file with content "some data" to "/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" copies a file "/textfile.txt" into "/folder" inside space "Personal" using file-id path "<dav-path>"
+    Then the HTTP status code should be "201"
+    And for user "Alice" the space "Personal" should contain these entries:
+      | folder/textfile.txt |
+      | textfile.txt        |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: copy a file into a sub-folder in personal space
+    Given user "Alice" has created folder "/folder"
+    And user "Alice" has created folder "folder/sub-folder"
+    And user "Alice" has uploaded file with content "some data" to "/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" copies a file "/textfile.txt" into "/folder/sub-folder" inside space "Personal" using file-id path "<dav-path>"
+    Then the HTTP status code should be "201"
+    And for user "Alice" the space "Personal" should contain these entries:
+      | folder/sub-folder/textfile.txt |
+      | textfile.txt                   |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: copy a file from a folder into root of personal space
+    Given user "Alice" has created folder "/folder"
+    And user "Alice" has uploaded file with content "some data" to "folder/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" copies a file "folder/textfile.txt" into "/" inside space "Personal" using file-id path "<dav-path>"
+    Then the HTTP status code should be "201"
+    And for user "Alice" the space "Personal" should contain these entries:
+      | textfile.txt        |
+      | folder/textfile.txt |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: copy a file from sub-folder into root of personal space
+    Given user "Alice" has created folder "/folder"
+    And user "Alice" has created folder "folder/sub-folder"
+    And user "Alice" has uploaded file with content "some data" to "folder/sub-folder/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" copies a file "folder/sub-folder/textfile.txt" into "/" inside space "Personal" using file-id path "<dav-path>"
+    Then the HTTP status code should be "201"
+    And for user "Alice" the space "Personal" should contain these entries:
+      | textfile.txt                   |
+      | folder/sub-folder/textfile.txt |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
diff --git a/tests/acceptance/features/apiSpacesDavOperation/propfindByFileId.feature b/tests/acceptance/features/apiSpacesDavOperation/propfindByFileId.feature
new file mode 100644
index 00000000000..b88a52af3dd
--- /dev/null
+++ b/tests/acceptance/features/apiSpacesDavOperation/propfindByFileId.feature
@@ -0,0 +1,137 @@
+Feature: propfind a file using file id
+  As a user
+  I want to check the PROPFIND response of file using their file id
+  So that I can make sure that the response contains all the relevant values
+
+  Background:
+    Given using spaces DAV path
+    And user "Alice" has been created with default attributes and without skeleton files
+
+
+  Scenario Outline: send a PROPFIND request to a file inside root of personal space
+    Given user "Alice" has uploaded file with content "some data" to "/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "207"
+    And the "PROPFIND" response to user "Alice" should contain a mountpoint "Alice Hansen" with these key and value pairs:
+      | key            | value        |
+      | oc:name        | textfile.txt |
+      | oc:permissions | RDNVWZP      |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: send a PROPFIND request to a file inside a folder of personal space
+    Given user "Alice" has created folder "folder"
+    And user "Alice" has uploaded file with content "some data" to "folder/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "207"
+    And the "PROPFIND" response to user "Alice" should contain a mountpoint "Alice Hansen" with these key and value pairs:
+      | key            | value        |
+      | oc:name        | textfile.txt |
+      | oc:permissions | RDNVWZP      |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: send a PROPFIND request to a file in personal space owned by another user
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And user "Alice" has uploaded file with content "some data" to "textfile.txt"
+    And we save it into "FILEID"
+    When user "Brian" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "404"
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: send a PROPFIND request to a file of inside project space
+    Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
+    And user "Alice" has created a space "new-space" with the default quota using the GraphApi
+    And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "207"
+    And the "PROPFIND" response should contain a space "new-space" with these key and value pairs:
+      | key            | value        |
+      | oc:name        | textfile.txt |
+      | oc:permissions | RDNVWZP      |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: send a PROPFIND request to a file inside a folder of project space
+    Given the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
+    And user "Alice" has created a space "new-space" with the default quota using the GraphApi
+    And user "Alice" has created a folder "folder" in space "new-space"
+    And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "/folder/textfile.txt"
+    And we save it into "FILEID"
+    When user "Alice" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "207"
+    And the "PROPFIND" response should contain a space "new-space" with these key and value pairs:
+      | key            | value        |
+      | oc:name        | textfile.txt |
+      | oc:permissions | RDNVWZP      |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: send a PROPFIND request to a file inside project space owned by another user
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And the administrator has assigned the role "Space Admin" to user "Alice" using the Graph API
+    And user "Alice" has created a space "new-space" with the default quota using the GraphApi
+    And user "Alice" has uploaded a file inside space "new-space" with content "some content" to "textfile.txt"
+    And we save it into "FILEID"
+    When user "Brian" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "404"
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: send a PROPFIND request to a shared file
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And user "Alice" has uploaded file with content "some data" to "/textfile.txt"
+    And we save it into "FILEID"
+    And user "Alice" has shared file "/textfile.txt" with user "Brian"
+    And user "Brian" has accepted share "/textfile.txt" offered by user "Alice"
+    When user "Brian" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "207"
+    And the "PROPFIND" response to user "Alice" should contain a mountpoint "Brian Murphy" with these key and value pairs:
+      | key            | value        |
+      | oc:name        | textfile.txt |
+      | oc:permissions | SRNVW        |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
+
+
+  Scenario Outline: sharee sends a PROPFIND request to a file inside of a shared folder
+    Given user "Brian" has been created with default attributes and without skeleton files
+    And user "Alice" has created folder "/folder"
+    And user "Alice" has shared folder "/folder" with user "Brian"
+    And user "Brian" has accepted share "/folder" offered by user "Alice"
+    And user "Alice" has uploaded file with content "some data" to "/folder/textfile.txt"
+    And we save it into "FILEID"
+    When user "Brian" sends HTTP method "PROPFIND" to URL "<dav-path>"
+    Then the HTTP status code should be "207"
+    And the "PROPFIND" response to user "Alice" should contain a mountpoint "Brian Murphy" with these key and value pairs:
+      | key            | value        |
+      | oc:name        | textfile.txt |
+      | oc:permissions | RDNVWZP      |
+    Examples:
+      | dav-path                          |
+      | /remote.php/dav/spaces/<<FILEID>> |
+      | /dav/spaces/<<FILEID>>            |
diff --git a/tests/acceptance/features/bootstrap/FeatureContext.php b/tests/acceptance/features/bootstrap/FeatureContext.php
index e7903b1a952..b437bda4ceb 100644
--- a/tests/acceptance/features/bootstrap/FeatureContext.php
+++ b/tests/acceptance/features/bootstrap/FeatureContext.php
@@ -3391,6 +3391,7 @@ class FeatureContext extends BehatVariablesContext {
 		if (\count(WebDavHelper::$spacesIdRef) > 0) {
 			WebDavHelper::$spacesIdRef = [];
 		}
+		WebDavHelper::$SPACE_ID_FROM_OCIS = '';
 	}
 
 	/**
diff --git a/tests/acceptance/features/bootstrap/NotificationContext.php b/tests/acceptance/features/bootstrap/NotificationContext.php
index e6b62b6436a..e5c694189f5 100644
--- a/tests/acceptance/features/bootstrap/NotificationContext.php
+++ b/tests/acceptance/features/bootstrap/NotificationContext.php
@@ -93,16 +93,14 @@ class NotificationContext implements Context {
 	}
 
 	/**
-	 * @When /^user "([^"]*)" lists all notifications$/
-	 *
 	 * @param string $user
 	 *
-	 * @return void
+	 * @return ResponseInterface
 	 */
-	public function userListAllNotifications(string $user):void {
+	public function listAllNotifications(string $user):ResponseInterface {
 		$this->setUserRecipient($user);
 		$headers = ["accept-language" => $this->settingsContext->getSettingLanguageValue($user)];
-		$response = OcsApiHelper::sendRequest(
+		return OcsApiHelper::sendRequest(
 			$this->featureContext->getBaseUrl(),
 			$this->featureContext->getActualUsername($user),
 			$this->featureContext->getPasswordForUser($user),
@@ -113,29 +111,52 @@ class NotificationContext implements Context {
 			2,
 			$headers
 		);
-		$this->featureContext->setResponse($response);
-		$this->featureContext->pushToLastHttpStatusCodesArray();
 	}
 
 	/**
-	 * @When user :user deletes all notifications
+	 * @When /^user "([^"]*)" lists all notifications$/
 	 *
 	 * @param string $user
 	 *
 	 * @return void
+	 */
+	public function userListAllNotifications(string $user):void {
+		$response = $this->listAllNotifications($user);
+		$this->featureContext->setResponse($response);
+		$this->featureContext->pushToLastHttpStatusCodesArray();
+	}
+
+	/**
+	 * @param string $user
+	 *
+	 * @return ResponseInterface
 	 * @throws GuzzleException
 	 * @throws JsonException
 	 */
-	public function userDeletesAllNotifications(string $user):void {
-		$this->userListAllNotifications($user);
-		if (isset($this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data)) {
-			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
+	public function deleteAllNotifications(string $user):ResponseInterface {
+		$response = $this->listAllNotifications($user);
+		if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
+			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
 			foreach ($responseBody as $value) {
 				// set notificationId
 				$this->notificationIds[] = $value->notification_id;
 			}
 		}
-		$this->featureContext->setResponse($this->userDeletesNotification($user));
+		return $this->userDeletesNotification($user);
+	}
+
+	/**
+	 * @When user :user deletes all notifications
+	 *
+	 * @param string $user
+	 *
+	 * @return void
+	 * @throws GuzzleException
+	 * @throws JsonException
+	 */
+	public function userDeletesAllNotifications(string $user):void {
+		$response = $this->deleteAllNotifications($user);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
@@ -148,8 +169,8 @@ class NotificationContext implements Context {
 	 * @throws JsonException
 	 */
 	public function userHasDeletedAllNotifications(string $user):void {
-		$this->userDeletesAllNotifications($user);
-		$this->featureContext->thenTheHTTPStatusCodeShouldBe(200);
+		$response = $this->deleteAllNotifications($user);
+		$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
 	}
 
 	/**
@@ -164,8 +185,8 @@ class NotificationContext implements Context {
 	 * @throws JsonException
 	 */
 	public function userDeletesNotificationOfResourceAndSubject(string $user, string $resource, string $subject):void {
-		$this->userListAllNotifications($user);
-		$this->filterResponseByNotificationSubjectAndResource($subject, $resource);
+		$response = $this->listAllNotifications($user);
+		$this->filterResponseByNotificationSubjectAndResource($subject, $resource, $response);
 		$this->featureContext->setResponse($this->userDeletesNotification($user));
 	}
 
@@ -174,7 +195,7 @@ class NotificationContext implements Context {
 	 *
 	 * @param string $user
 	 *
-	 * @return void
+	 * @return ResponseInterface
 	 * @throws GuzzleException
 	 * @throws JsonException
 	 */
@@ -281,13 +302,14 @@ class NotificationContext implements Context {
 	 * filter notification according to subject
 	 *
 	 * @param string $subject
+	 * @param ResponseInterface|null $response
 	 *
 	 * @return object
 	 */
-	public function filterResponseAccordingToNotificationSubject(string $subject): object {
-		$responseBody =  null;
-		if (isset($this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data)) {
-			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
+	public function filterResponseAccordingToNotificationSubject(string $subject, ?ResponseInterface $response = null): object {
+		$response = $response ?? $this->featureContext->getResponse();
+		if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
+			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
 			foreach ($responseBody as $value) {
 				if (isset($value->subject) && $value->subject === $subject) {
 					$responseBody = $value;
@@ -297,7 +319,7 @@ class NotificationContext implements Context {
 				}
 			}
 		} else {
-			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent();
+			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response);
 		}
 		return $responseBody;
 	}
@@ -307,18 +329,20 @@ class NotificationContext implements Context {
 	 *
 	 * @param string $subject
 	 * @param string $resource
+	 * @param ResponseInterface|null $response
 	 *
 	 * @return array
 	 */
-	public function filterResponseByNotificationSubjectAndResource(string $subject, string $resource): array {
+	public function filterResponseByNotificationSubjectAndResource(string $subject, string $resource, ?ResponseInterface $response = null): array {
 		$responseBodyArray = [];
-		$statusCode = $this->featureContext->getResponse()->getStatusCode();
+		$response = $response ?? $this->featureContext->getResponse();
+		$statusCode = $response->getStatusCode();
 		if ($statusCode !== 200) {
-			$response = $this->featureContext->getResponse()->getBody()->getContents();
+			$response = $response->getBody()->getContents();
 			Assert::fail($response . " Response should contain status code 200");
 		}
-		if (isset($this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data)) {
-			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent()->ocs->data;
+		if (isset($this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data)) {
+			$responseBody = $this->featureContext->getJsonDecodedResponseBodyContent($response)->ocs->data;
 			foreach ($responseBody as $value) {
 				if (isset($value->subject) && $value->subject === $subject && isset($value->messageRichParameters->resource->name) && $value->messageRichParameters->resource->name === $resource) {
 					$this->notificationIds[] = $value->notification_id;
@@ -326,7 +350,7 @@ class NotificationContext implements Context {
 				}
 			}
 		} else {
-			$responseBodyArray[] = $this->featureContext->getJsonDecodedResponseBodyContent();
+			$responseBodyArray[] = $this->featureContext->getJsonDecodedResponseBodyContent($response);
 			Assert::fail("Response should contain notification but found: $responseBodyArray");
 		}
 		return $responseBodyArray;
@@ -351,12 +375,12 @@ class NotificationContext implements Context {
 				\sleep(1);
 			}
 			$this->featureContext->setResponse(null);
-			$this->userListAllNotifications($user);
-			$this->featureContext->theHTTPStatusCodeShouldBe(200);
+			$response = $this->listAllNotifications($user);
+			$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
 			++$count;
-		} while (!isset($this->filterResponseAccordingToNotificationSubject($subject)->message) && $count <= 5);
-		if (isset($this->filterResponseAccordingToNotificationSubject($subject)->message)) {
-			$actualMessage = str_replace(["\r", "\n"], " ", $this->filterResponseAccordingToNotificationSubject($subject)->message);
+		} while (!isset($this->filterResponseAccordingToNotificationSubject($subject, $response)->message) && $count <= 5);
+		if (isset($this->filterResponseAccordingToNotificationSubject($subject, $response)->message)) {
+			$actualMessage = str_replace(["\r", "\n"], " ", $this->filterResponseAccordingToNotificationSubject($subject, $response)->message);
 		} else {
 			throw new \Exception("Notification was not found even after retrying for 5 seconds.");
 		}
@@ -380,8 +404,8 @@ class NotificationContext implements Context {
 	 * @throws Exception
 	 */
 	public function userShouldGetNotificationForResourceWithMessage(string $user, string $resource, string $subject, TableNode $table):void {
-		$this->userListAllNotifications($user);
-		$notification = $this->filterResponseByNotificationSubjectAndResource($subject, $resource);
+		$response = $this->listAllNotifications($user);
+		$notification = $this->filterResponseByNotificationSubjectAndResource($subject, $resource, $response);
 
 		if (\count($notification) === 1) {
 			$actualMessage = str_replace(["\r", "\r"], " ", $notification[0]->message);
@@ -410,9 +434,9 @@ class NotificationContext implements Context {
 	 * @return void
 	 */
 	public function userShouldNotHaveANotificationRelatedToResourceWithSubject(string $user, string $resource, string $subject):void {
-		$this->userListAllNotifications($user);
-		$response = $this->filterResponseByNotificationSubjectAndResource($subject, $resource);
-		Assert::assertCount(0, $response, "Response should not contain notification related to resource '$resource' with subject '$subject' but found" . print_r($response, true));
+		$response = $this->listAllNotifications($user);
+		$filteredResponse = $this->filterResponseByNotificationSubjectAndResource($subject, $resource, $response);
+		Assert::assertCount(0, $filteredResponse, "Response should not contain notification related to resource '$resource' with subject '$subject' but found" . print_r($filteredResponse, true));
 	}
 
 	/**
@@ -524,7 +548,7 @@ class NotificationContext implements Context {
 	 * @param string|null $deprovision_date
 	 * @param string|null $deprovision_date_format
 	 *
-	 * @return void
+	 * @return ResponseInterface
 	 *
 	 * @throws GuzzleException
 	 *
@@ -589,8 +613,8 @@ class NotificationContext implements Context {
 	 * @return void
 	 */
 	public function userHasCreatedDeprovisioningNotification():void {
-		$this->userCreatesDeprovisioningNotification();
-		$this->featureContext->thenTheHTTPStatusCodeShouldBe(200);
+		$response = $this->userCreatesDeprovisioningNotification();
+		$this->featureContext->theHTTPStatusCodeShouldBe(200, "", $response);
 	}
 
 	/**
diff --git a/tests/acceptance/features/bootstrap/SettingsContext.php b/tests/acceptance/features/bootstrap/SettingsContext.php
index 01c847166bd..f9b3d7ab6c5 100644
--- a/tests/acceptance/features/bootstrap/SettingsContext.php
+++ b/tests/acceptance/features/bootstrap/SettingsContext.php
@@ -349,12 +349,12 @@ class SettingsContext implements Context {
 	 * @param string $user
 	 * @param string $language
 	 *
-	 * @return void
+	 * @return ResponseInterface
 	 *
 	 * @throws GuzzleException
 	 * @throws Exception
 	 */
-	public function sendRequestToSwitchSystemLanguage(string $user, string $language): void {
+	public function sendRequestToSwitchSystemLanguage(string $user, string $language): ResponseInterface {
 		$profileBundlesList = $this->getBundlesList($user, "Profile");
 		Assert::assertNotEmpty($profileBundlesList, "bundles list is empty");
 
@@ -390,9 +390,12 @@ class SettingsContext implements Context {
 			],
 			JSON_THROW_ON_ERROR
 		);
-
-		$this->featureContext->setResponse(
-			$this->spacesContext->sendPostRequestToUrl($fullUrl, $user, $this->featureContext->getPasswordForUser($user), $body, $this->featureContext->getStepLineRef())
+		return $this->spacesContext->sendPostRequestToUrl(
+			$fullUrl,
+			$user,
+			$this->featureContext->getPasswordForUser($user),
+			$body,
+			$this->featureContext->getStepLineRef()
 		);
 	}
 
@@ -405,12 +408,14 @@ class SettingsContext implements Context {
 	 * @return void
 	 *
 	 * @throws Exception
+	 * @throws GuzzleException
 	 */
-	public function theUserHasSwitchedSysemLanguage(string $user, string $language): void {
-		$this->sendRequestToSwitchSystemLanguage($user, $language);
+	public function theUserHasSwitchedSystemLanguage(string $user, string $language): void {
+		$response = $this->sendRequestToSwitchSystemLanguage($user, $language);
 		$this->featureContext->theHTTPStatusCodeShouldBe(
 			201,
-			"Expected response status code should be 201"
+			"Expected response status code should be 201",
+			$response
 		);
 	}
 }
diff --git a/tests/acceptance/features/bootstrap/SpacesContext.php b/tests/acceptance/features/bootstrap/SpacesContext.php
index 87f21cf4599..4c9fb4a8113 100644
--- a/tests/acceptance/features/bootstrap/SpacesContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesContext.php
@@ -1753,15 +1753,19 @@ class SpacesContext implements Context {
 	 * @param string $user
 	 * @param string $fileDestination
 	 * @param string $spaceName
+	 * @param string|null $endPath
 	 *
 	 * @return string
 	 * @throws GuzzleException
 	 */
-	public function destinationHeaderValueWithSpaceName(string $user, string $fileDestination, string $spaceName):string {
+	public function destinationHeaderValueWithSpaceName(string $user, string $fileDestination, string $spaceName, string $endPath = null):string {
 		$space = $this->getSpaceByName($user, $spaceName);
-
 		$fileDestination = $this->escapePath(\ltrim($fileDestination, "/"));
-
+		if ($endPath && str_contains($endPath, 'remote.php')) {
+			// this is check for when we want to test with the endpoint having `remote.php` in  space webdav
+			// by default spaces webdav is '/dav/spaces'
+			return $this->featureContext->getBaseUrl() . '/remote.php/dav/spaces/' . $space['id'] . '/' . $fileDestination;
+		}
 		return $space["root"]["webDavUrl"] . '/' . $fileDestination;
 	}
 
@@ -1786,6 +1790,27 @@ class SpacesContext implements Context {
 		);
 	}
 
+	/**
+	 * @When /^user "([^"]*)" copies a file "([^"]*)" into "([^"]*)" inside space "([^"]*)" using file-id path "([^"]*)"$/
+	 *
+	 * @param string $user
+	 * @param string $sourceFile
+	 * @param string $destinationFile
+	 * @param string $toSpaceName
+	 * @param string $url
+	 *
+	 * @throws GuzzleException
+	 * @return void
+	 */
+	public function userCopiesFileWithFileIdFromAndToSpaceBetweenSpaces(string $user, string $sourceFile, string $destinationFile, string $toSpaceName, string $url): void {
+		// split the source when there are sub-folders
+		$sourceFile = explode("/", $sourceFile);
+		$fileDestination = $this->escapePath(\ltrim($destinationFile, "/")) . '/' . $this->escapePath(\ltrim(end($sourceFile), "/"));
+		$headers['Destination'] = $this->destinationHeaderValueWithSpaceName($user, $fileDestination, $toSpaceName, $url);
+		$fullUrl = $this->featureContext->getBaseUrl() . $url;
+		$this->featureContext->setResponse($this->copyFilesAndFoldersRequest($user, $fullUrl, $headers));
+	}
+
 	/**
 	 * @Given /^user "([^"]*)" has uploaded a file inside space "([^"]*)" with content "([^"]*)" to "([^"]*)"$/
 	 *
diff --git a/tests/acceptance/features/bootstrap/SpacesTUSContext.php b/tests/acceptance/features/bootstrap/SpacesTUSContext.php
index 9b53a034546..d8234f27cbf 100644
--- a/tests/acceptance/features/bootstrap/SpacesTUSContext.php
+++ b/tests/acceptance/features/bootstrap/SpacesTUSContext.php
@@ -126,7 +126,8 @@ class SpacesTUSContext implements Context {
 		TableNode $headers
 	): void {
 		$this->spacesContext->setSpaceIDByName($user, $spaceName);
-		$this->tusContext->createNewTUSResourceWithHeaders($user, $headers, $content);
+		$response = $this->tusContext->createNewTUSResourceWithHeaders($user, $headers, $content);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
diff --git a/tests/acceptance/features/bootstrap/TUSContext.php b/tests/acceptance/features/bootstrap/TUSContext.php
index eca3ea69ca1..57b912d3bb1 100644
--- a/tests/acceptance/features/bootstrap/TUSContext.php
+++ b/tests/acceptance/features/bootstrap/TUSContext.php
@@ -30,6 +30,7 @@ use TusPhp\Exception\ConnectionException;
 use TusPhp\Exception\TusException;
 use TusPhp\Tus\Client;
 use PHPUnit\Framework\Assert;
+use Psr\Http\Message\ResponseInterface;
 
 require_once 'bootstrap.php';
 
@@ -42,40 +43,54 @@ class TUSContext implements Context {
 	private ?string $resourceLocation = null;
 
 	/**
-	 * @When user :user creates a new TUS resource on the WebDAV API with these headers:
-	 *
 	 * @param string $user
 	 * @param TableNode $headers
 	 * @param string $content
 	 *
-	 * @return void
+	 * @return ResponseInterface
 	 *
 	 * @throws Exception
 	 * @throws GuzzleException
 	 */
-	public function createNewTUSResourceWithHeaders(string $user, TableNode $headers, string $content = ''): void {
+	public function createNewTUSResourceWithHeaders(string $user, TableNode $headers, string $content = ''): ResponseInterface {
 		$this->featureContext->verifyTableNodeColumnsCount($headers, 2);
 		$user = $this->featureContext->getActualUsername($user);
 		$password = $this->featureContext->getUserPassword($user);
 		$this->resourceLocation = null;
 
-		$this->featureContext->setResponse(
-			$this->featureContext->makeDavRequest(
-				$user,
-				"POST",
-				null,
-				$headers->getRowsHash(),
-				$content,
-				"files",
-				null,
-				false,
-				$password
-			)
+		$response = $this->featureContext->makeDavRequest(
+			$user,
+			"POST",
+			null,
+			$headers->getRowsHash(),
+			$content,
+			"files",
+			null,
+			false,
+			$password
 		);
-		$locationHeader = $this->featureContext->getResponse()->getHeader('Location');
+		$locationHeader = $response->getHeader('Location');
 		if (\sizeof($locationHeader) > 0) {
 			$this->resourceLocation = $locationHeader[0];
 		}
+		return $response;
+	}
+
+	/**
+	 * @When user :user creates a new TUS resource on the WebDAV API with these headers:
+	 *
+	 * @param string $user
+	 * @param TableNode $headers
+	 * @param string $content
+	 *
+	 * @return void
+	 *
+	 * @throws Exception
+	 * @throws GuzzleException
+	 */
+	public function userCreateNewTUSResourceWithHeaders(string $user, TableNode $headers, string $content = ''): void {
+		$response = $this->createNewTUSResourceWithHeaders($user, $headers, $content);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
@@ -89,46 +104,58 @@ class TUSContext implements Context {
 	 * @throws Exception
 	 * @throws GuzzleException
 	 */
-	public function createNewTUSResource(string $user, TableNode $headers): void {
+	public function userHasCreatedNewTUSResourceWithHeaders(string $user, TableNode $headers): void {
 		$rows = $headers->getRows();
 		$rows[] = ['Tus-Resumable', '1.0.0'];
-		$this->createNewTUSResourceWithHeaders($user, new TableNode($rows));
-		$this->featureContext->theHTTPStatusCodeShouldBe(201);
+		$response = $this->createNewTUSResourceWithHeaders($user, new TableNode($rows));
+		$this->featureContext->theHTTPStatusCodeShouldBe(201, "", $response);
 	}
 
 	/**
-	 * @When /^user "([^"]*)" sends a chunk to the last created TUS Location with offset "([^"]*)" and data "([^"]*)" using the WebDAV API$/
-	 *
 	 * @param string $user
 	 * @param string $offset
 	 * @param string $data
 	 * @param string $checksum
 	 *
-	 * @return void
+	 * @return ResponseInterface
 	 *
 	 * @throws GuzzleException
 	 * @throws JsonException
 	 */
-	public function sendsAChunkToTUSLocationWithOffsetAndData(string $user, string $offset, string $data, string $checksum = ''): void {
+	public function sendsAChunkToTUSLocationWithOffsetAndData(string $user, string $offset, string $data, string $checksum = ''): ResponseInterface {
 		$user = $this->featureContext->getActualUsername($user);
 		$password = $this->featureContext->getUserPassword($user);
-		$this->featureContext->setResponse(
-			HttpRequestHelper::sendRequest(
-				$this->resourceLocation,
-				$this->featureContext->getStepLineRef(),
-				'PATCH',
-				$user,
-				$password,
-				[
-					'Content-Type' => 'application/offset+octet-stream',
-					'Tus-Resumable' => '1.0.0',
-					'Upload-Checksum' => $checksum,
-					'Upload-Offset' => $offset
-				],
-				$data
-			)
+		return HttpRequestHelper::sendRequest(
+			$this->resourceLocation,
+			$this->featureContext->getStepLineRef(),
+			'PATCH',
+			$user,
+			$password,
+			[
+				'Content-Type' => 'application/offset+octet-stream',
+				'Tus-Resumable' => '1.0.0',
+				'Upload-Checksum' => $checksum,
+				'Upload-Offset' => $offset
+			],
+			$data
 		);
-		WebDavHelper::$SPACE_ID_FROM_OCIS = '';
+	}
+
+	/**
+	 * @When user :user sends a chunk to the last created TUS Location with offset :offset and data :data using the WebDAV API
+	 *
+	 * @param string $user
+	 * @param string $offset
+	 * @param string $data
+	 *
+	 * @return void
+	 *
+	 * @throws GuzzleException
+	 * @throws JsonException
+	 */
+	public function userSendsAChunkToTUSLocationWithOffsetAndData(string $user, string $offset, string $data): void {
+		$response = $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
@@ -359,7 +386,8 @@ class TUSContext implements Context {
 		string    $content,
 		TableNode $headers
 	): void {
-		$this->createNewTUSResourceWithHeaders($user, $headers, $content);
+		$response = $this->createNewTUSResourceWithHeaders($user, $headers, $content);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
@@ -407,7 +435,8 @@ class TUSContext implements Context {
 		string $offset,
 		string $content
 	): void {
-		$this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $content, $checksum);
+		$response = $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $content, $checksum);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
@@ -427,8 +456,8 @@ class TUSContext implements Context {
 		string $offset,
 		string $content
 	): void {
-		$this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $content, $checksum);
-		$this->featureContext->theHTTPStatusCodeShouldBe(204);
+		$response = $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $content, $checksum);
+		$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
 	}
 
 	/**
@@ -443,7 +472,8 @@ class TUSContext implements Context {
 	 * @throws Exception
 	 */
 	public function userUploadsChunkFileWithChecksum(string $user, string $offset, string $data, string $checksum): void {
-		$this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum);
+		$response = $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum);
+		$this->featureContext->setResponse($response);
 	}
 
 	/**
@@ -458,8 +488,8 @@ class TUSContext implements Context {
 	 * @throws Exception
 	 */
 	public function userHasUploadedChunkFileWithChecksum(string $user, string $offset, string $data, string $checksum): void {
-		$this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum);
-		$this->featureContext->theHTTPStatusCodeShouldBe(204);
+		$response = $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum);
+		$this->featureContext->theHTTPStatusCodeShouldBe(204, "", $response);
 	}
 
 	/**
@@ -478,7 +508,8 @@ class TUSContext implements Context {
 	 * @throws Exception
 	 */
 	public function userOverwritesFileWithChecksum(string $user, string $offset, string $data, string $checksum, TableNode $headers): void {
-		$this->createNewTUSResource($user, $headers);
-		$this->userHasUploadedChunkFileWithChecksum($user, $offset, $data, $checksum);
+		$this->userHasCreatedNewTUSResourceWithHeaders($user, $headers);
+		$response = $this->sendsAChunkToTUSLocationWithOffsetAndData($user, $offset, $data, $checksum);
+		$this->featureContext->setResponse($response);
 	}
 }