fix: scim tests failing

This commit is contained in:
Marco Ardizzone
2025-08-11 12:22:45 +02:00
parent 9ec3430670
commit 6522b2b726
13 changed files with 619 additions and 488 deletions

View File

@@ -17,6 +17,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/text/language" "golang.org/x/text/language"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/api/scim/resources"
"github.com/zitadel/zitadel/internal/api/scim/schemas" "github.com/zitadel/zitadel/internal/api/scim/schemas"
@@ -478,7 +480,7 @@ func TestBulk(t *testing.T) {
}, },
{ {
name: "fail on errors", name: "fail on errors",
body: bulkFailOnErrorsJson, body: withUsername(bulkFailOnErrorsJson, gofakeit.Username()),
want: &scim.BulkResponse{ want: &scim.BulkResponse{
Schemas: []schemas.ScimSchemaType{schemas.IdBulkResponse}, Schemas: []schemas.ScimSchemaType{schemas.IdBulkResponse},
Operations: []*scim.BulkResponseOperation{ Operations: []*scim.BulkResponseOperation{
@@ -579,7 +581,6 @@ func TestBulk(t *testing.T) {
response, err := Instance.Client.SCIM.Bulk(ctx, orgID, tt.body) response, err := Instance.Client.SCIM.Bulk(ctx, orgID, tt.body)
createdUserIDs := buildCreatedIDs(response) createdUserIDs := buildCreatedIDs(response)
defer deleteUsers(t, createdUserIDs)
if tt.wantErr != nil { if tt.wantErr != nil {
statusCode := tt.wantErr.status statusCode := tt.wantErr.status
@@ -656,17 +657,6 @@ func buildCreatedIDs(response *scim.BulkResponse) []string {
return createdIds return createdIds
} }
func deleteUsers(t require.TestingT, ids []string) {
for _, id := range ids {
err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, id)
// only not found errors are ok (if the user is deleted in a later on bulk request)
if err != nil {
scim.RequireScimError(t, http.StatusNotFound, err)
}
}
}
func buildMinimalUpdateRequest(userID string) *scim.BulkRequest { func buildMinimalUpdateRequest(userID string) *scim.BulkRequest {
return &scim.BulkRequest{ return &scim.BulkRequest{
Schemas: []schemas.ScimSchemaType{schemas.IdBulkRequest}, Schemas: []schemas.ScimSchemaType{schemas.IdBulkRequest},
@@ -690,7 +680,7 @@ func buildTooManyOperationsRequest() *scim.BulkRequest {
req.Operations[i] = &scim.BulkRequestOperation{ req.Operations[i] = &scim.BulkRequestOperation{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/Users", Path: "/Users",
Data: minimalUserJson, Data: withUsername(minimalUserJson, gofakeit.Username()),
} }
} }
@@ -720,8 +710,11 @@ func ensureMetadataProjected(t require.TestingT, userID, key, value string) {
Id: userID, Id: userID,
Key: key, Key: key,
}) })
require.NoError(tt, err) if !assert.NoError(tt, err) {
require.Equal(tt, value, string(md.Metadata.Value)) require.Equal(tt, status.Code(err), codes.NotFound)
return
}
assert.Equal(tt, value, string(md.Metadata.Value))
}, retryDuration, tick) }, retryDuration, tick)
} }

View File

@@ -32,14 +32,14 @@
"urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:core:2.0:User"
], ],
"externalId": "scim-bulk-created-user-0", "externalId": "scim-bulk-created-user-0",
"userName": "scim-bulk-created-user-0", "userName": "{{ .Username }}",
"name": { "name": {
"familyName": "scim-bulk-created-user-0-family-name", "familyName": "scim-bulk-created-user-0-family-name",
"givenName": "scim-bulk-created-user-0-given-name" "givenName": "scim-bulk-created-user-0-given-name"
}, },
"emails": [ "emails": [
{ {
"value": "scim-bulk-created-user-0@example.com", "value": "{{ .Username }}@example.com",
"primary": true "primary": true
} }
], ],

View File

@@ -1,7 +1,7 @@
{ {
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": "701984", "externalId": "701984",
"userName": "bjensen@example.com", "userName": "{{ .Username }}@example.com",
"name": { "name": {
"formatted": "Ms. Barbara J Jensen, III", "formatted": "Ms. Barbara J Jensen, III",
"familyName": "Jensen", "familyName": "Jensen",
@@ -15,12 +15,12 @@
"profileUrl": "http://login.example.com/bjensen", "profileUrl": "http://login.example.com/bjensen",
"emails": [ "emails": [
{ {
"value": "bjensen@example.com", "value": "{{ .Username }}@example.com",
"type": "work", "type": "work",
"primary": true "primary": true
}, },
{ {
"value": "babs@jensen.org", "value": "{{ .Username }}+1@example.com",
"type": "home" "type": "home"
} }
], ],

View File

@@ -2,14 +2,14 @@
"schemas": [ "schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:core:2.0:User"
], ],
"userName": "acmeUser1", "userName": "{{ .Username }}",
"name": { "name": {
"familyName": "Ross", "familyName": "Ross",
"givenName": "Bethany" "givenName": "Bethany"
}, },
"emails": [ "emails": [
{ {
"value": "user1@example.com", "value": "{{ .Username }}@example.com",
"primary": true "primary": true
} }
] ]

View File

@@ -2,14 +2,14 @@
"schemas": [ "schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:core:2.0:User"
], ],
"userName": "acmeUser1", "userName": "acmeUser1-inactive",
"name": { "name": {
"familyName": "Ross", "familyName": "Ross",
"givenName": "Bethany" "givenName": "Bethany"
}, },
"emails": [ "emails": [
{ {
"value": "user1@example.com", "value": "user1-inactive@example.com",
"primary": true "primary": true
} }
], ],

View File

@@ -2,14 +2,14 @@
"schemas": [ "schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:core:2.0:User"
], ],
"userName": "acmeUser1", "userName": "acmeUser1-no-primary-email-phone",
"name": { "name": {
"familyName": "Ross", "familyName": "Ross",
"givenName": "Bethany" "givenName": "Bethany"
}, },
"emails": [ "emails": [
{ {
"value": "user1@example.com" "value": "user1-no-primary-email-phone@example.com"
} }
], ],
"phoneNumbers": [ "phoneNumbers": [

View File

@@ -2,14 +2,14 @@
"schemas": [ "schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User" "urn:ietf:params:scim:schemas:core:2.0:User"
], ],
"userName": "acmeUser1-minimal-replaced", "userName": "{{ .Username }}",
"name": { "name": {
"familyName": "Ross-replaced", "familyName": "Ross-replaced",
"givenName": "Bethany-replaced" "givenName": "Bethany-replaced"
}, },
"emails": [ "emails": [
{ {
"value": "user1-minimal-replaced@example.com", "value": "{{ .Username }}@example.com",
"primary": true, "primary": true,
"type": "work" "type": "work"
} }

View File

@@ -7,7 +7,7 @@
"value": { "value": {
"emails":[ "emails":[
{ {
"value":"babs@example.com", "value":"{{ .Username }}+2@example.com",
"type":"home", "type":"home",
"primary": true "primary": true
} }

View File

@@ -3,17 +3,22 @@
package integration_test package integration_test
import ( import (
"bytes"
"context" "context"
_ "embed" _ "embed"
"fmt"
"net/http" "net/http"
"path" "path"
"testing" "testing"
"text/template"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/text/language" "golang.org/x/text/language"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/api/scim/resources"
"github.com/zitadel/zitadel/internal/api/scim/schemas" "github.com/zitadel/zitadel/internal/api/scim/schemas"
@@ -157,7 +162,18 @@ var (
} }
) )
func withUsername(fixture []byte, username string) []byte {
buf := new(bytes.Buffer)
template.Must(template.New("").Parse(string(fixture))).Execute(buf, &struct {
Username string
}{
Username: username,
})
return buf.Bytes()
}
func TestCreateUser(t *testing.T) { func TestCreateUser(t *testing.T) {
minimalUsername := gofakeit.Username()
tests := []struct { tests := []struct {
name string name string
body []byte body []byte
@@ -171,16 +187,16 @@ func TestCreateUser(t *testing.T) {
}{ }{
{ {
name: "minimal user", name: "minimal user",
body: minimalUserJson, body: withUsername(minimalUserJson, minimalUsername),
want: &resources.ScimUser{ want: &resources.ScimUser{
UserName: "acmeUser1", UserName: minimalUsername,
Name: &resources.ScimUserName{ Name: &resources.ScimUserName{
FamilyName: "Ross", FamilyName: "Ross",
GivenName: "Bethany", GivenName: "Bethany",
}, },
Emails: []*resources.ScimEmail{ Emails: []*resources.ScimEmail{
{ {
Value: "user1@example.com", Value: minimalUsername + "@example.com",
Primary: true, Primary: true,
}, },
}, },
@@ -195,7 +211,7 @@ func TestCreateUser(t *testing.T) {
}, },
{ {
name: "full user", name: "full user",
body: fullUserJson, body: withUsername(fullUserJson, "bjensen"),
want: fullUser, want: fullUser,
}, },
{ {
@@ -204,7 +220,7 @@ func TestCreateUser(t *testing.T) {
want: &resources.ScimUser{ want: &resources.ScimUser{
Emails: []*resources.ScimEmail{ Emails: []*resources.ScimEmail{
{ {
Value: "user1@example.com", Value: "user1-no-primary-email-phone@example.com",
Primary: true, Primary: true,
}, },
}, },
@@ -262,21 +278,21 @@ func TestCreateUser(t *testing.T) {
}, },
{ {
name: "not authenticated", name: "not authenticated",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: context.Background(), ctx: context.Background(),
wantErr: true, wantErr: true,
errorStatus: http.StatusUnauthorized, errorStatus: http.StatusUnauthorized,
}, },
{ {
name: "no permissions", name: "no permissions",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}, },
{ {
name: "another org", name: "another org",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
orgID: SecondaryOrganization.OrganizationId, orgID: SecondaryOrganization.OrganizationId,
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
@@ -315,11 +331,6 @@ func TestCreateUser(t *testing.T) {
} }
assert.NotEmpty(t, createdUser.ID) assert.NotEmpty(t, createdUser.ID)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
assert.NoError(t, err)
}()
assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas) assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas)
assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType) assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType)
assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", createdUser.ID), createdUser.Resource.Meta.Location) assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", createdUser.ID), createdUser.Resource.Meta.Location)
@@ -345,10 +356,11 @@ func TestCreateUser(t *testing.T) {
} }
func TestCreateUser_duplicate(t *testing.T) { func TestCreateUser_duplicate(t *testing.T) {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson) parsedMinimalUserJson := withUsername(minimalUserJson, gofakeit.Username())
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson)
require.NoError(t, err) require.NoError(t, err)
_, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson) _, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson)
scimErr := scim.RequireScimError(t, http.StatusConflict, err) scimErr := scim.RequireScimError(t, http.StatusConflict, err)
assert.Equal(t, "User already exists", scimErr.Error.Detail) assert.Equal(t, "User already exists", scimErr.Error.Detail)
assert.Equal(t, "uniqueness", scimErr.Error.ScimType) assert.Equal(t, "uniqueness", scimErr.Error.ScimType)
@@ -358,14 +370,10 @@ func TestCreateUser_duplicate(t *testing.T) {
} }
func TestCreateUser_metadata(t *testing.T) { func TestCreateUser_metadata(t *testing.T) {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) username := gofakeit.Username()
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err) require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
}()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) { require.EventuallyWithT(t, func(tt *assert.CollectT) {
md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{ md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{
@@ -391,38 +399,111 @@ func TestCreateUser_metadata(t *testing.T) {
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:locale", "en-US") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:locale", "en-US")
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:ims", `[{"value":"someaimhandle","type":"aim"},{"value":"twitterhandle","type":"X"}]`) test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:ims", `[{"value":"someaimhandle","type":"aim"},{"value":"twitterhandle","type":"X"}]`)
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:roles", `[{"value":"my-role-1","display":"Rolle 1","type":"main-role","primary":true},{"value":"my-role-2","display":"Rolle 2","type":"secondary-role"}]`) test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:roles", `[{"value":"my-role-1","display":"Rolle 1","type":"main-role","primary":true},{"value":"my-role-2","display":"Rolle 2","type":"secondary-role"}]`)
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", `[{"value":"bjensen@example.com","primary":true,"type":"work"},{"value":"babs@jensen.org","primary":false,"type":"home"}]`) test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf(`[{"value":"%s@example.com","primary":true,"type":"work"},{"value":"%s+1@example.com","primary":false,"type":"home"}]`, username, username))
}, retryDuration, tick) }, retryDuration, tick)
} }
func TestCreateUser_scopedExternalID(t *testing.T) { func TestCreateUser_scopedExternalID(t *testing.T) {
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
require.NoError(t, err) require.NoError(t, err)
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
defer func() { setProvisioningDomain(t, callingUserId, "fooBar")
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err) require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
}()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) { require.EventuallyWithT(t, func(tt *assert.CollectT) {
// unscoped externalID should not exist // unscoped externalID should not exist
_, err = Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{ unscoped, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{
Id: createdUser.ID, Id: createdUser.ID,
Key: "urn:zitadel:scim:externalId", Key: "urn:zitadel:scim:externalId",
}) })
integration.AssertGrpcStatus(tt, codes.NotFound, err) integration.AssertGrpcStatus(tt, codes.NotFound, err)
unscoped = unscoped
// scoped externalID should exist // scoped externalID should exist
md, err := Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{ md, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{
Id: createdUser.ID, Id: createdUser.ID,
Key: "urn:zitadel:scim:fooBar:externalId", Key: "urn:zitadel:scim:fooBar:externalId",
}) })
require.NoError(tt, err) if !assert.NoError(tt, err) {
require.Equal(tt, status.Code(err), codes.NotFound)
return
}
assert.Equal(tt, "701984", string(md.Metadata.Value)) assert.Equal(tt, "701984", string(md.Metadata.Value))
}, retryDuration, tick) }, retryDuration, tick)
} }
func TestCreateUser_ignorePasswordOnCreate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ignorePassword string
scimErrorType string
scimErrorDetail string
wantUser *resources.ScimUser
wantErr bool
}{
{
name: "ignorePasswordOnCreate set to false",
ignorePassword: "false",
wantErr: true,
scimErrorType: "invalidValue",
scimErrorDetail: "Password is too short",
},
{
name: "ignorePasswordOnCreate set to an invalid value",
ignorePassword: "random",
wantErr: true,
scimErrorType: "invalidValue",
scimErrorDetail: "Invalid value for metadata key urn:zitadel:scim:ignorePasswordOnCreate: random",
},
{
name: "ignorePasswordOnCreate set to true",
ignorePassword: "true",
wantUser: &resources.ScimUser{
UserName: "acmeUser1",
Name: &resources.ScimUserName{
FamilyName: "Ross",
GivenName: "Bethany",
},
Emails: []*resources.ScimEmail{
{
Value: "user1@example.com",
Primary: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// create a machine user
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
require.NoError(t, err)
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
// set urn:zitadel:scim:ignorePasswordOnCreate metadata for the machine user
setAndEnsureMetadata(t, callingUserId, "urn:zitadel:scim:ignorePasswordOnCreate", tt.ignorePassword)
// create a user with an invalid password
createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(invalidPasswordUserJson, "acmeUser1"))
require.Equal(t, tt.wantErr, err != nil)
if err != nil {
scimErr := scim.RequireScimError(t, http.StatusBadRequest, err)
assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType)
assert.Equal(t, tt.scimErrorDetail, scimErr.Error.Detail)
return
}
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
// ensure the user is really stored and not just returned to the caller
fetchedUser, err := Instance.Client.SCIM.Users.Get(CTX, Instance.DefaultOrg.Id, createdUser.ID)
require.NoError(ttt, err)
assert.True(ttt, test.PartiallyDeepEqual(tt.wantUser, fetchedUser))
}, retryDuration, tick)
})
}
}

View File

@@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/text/language" "golang.org/x/text/language"
@@ -18,55 +19,76 @@ import (
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
) )
func TestGetUser(t *testing.T) { func TestGetUser(t *testing.T) {
tests := []struct { type testCase struct {
name string
orgID string
buildUserID func() string
cleanup func(userID string)
ctx context.Context ctx context.Context
orgID string
userID string
want *resources.ScimUser want *resources.ScimUser
wantErr bool wantErr bool
errorStatus int errorStatus int
}
tests := []struct {
name string
setup func(t *testing.T) testCase
}{ }{
{ {
name: "not authenticated", name: "not authenticated",
setup: func(t *testing.T) testCase {
return testCase{
ctx: context.Background(), ctx: context.Background(),
errorStatus: http.StatusUnauthorized, errorStatus: http.StatusUnauthorized,
wantErr: true, wantErr: true,
}
},
}, },
{ {
name: "no permissions", name: "no permissions",
setup: func(t *testing.T) testCase {
return testCase{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
wantErr: true, wantErr: true,
}
},
}, },
{ {
name: "another org", name: "another org",
setup: func(t *testing.T) testCase {
return testCase{
orgID: SecondaryOrganization.OrganizationId, orgID: SecondaryOrganization.OrganizationId,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
wantErr: true, wantErr: true,
}
},
}, },
{ {
name: "another org with permissions", name: "another org with permissions",
orgID: SecondaryOrganization.OrganizationId, setup: func(t *testing.T) testCase {
return testCase{
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
orgID: SecondaryOrganization.OrganizationId,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
wantErr: true, wantErr: true,
}
},
}, },
{ {
name: "unknown user id", name: "unknown user id",
buildUserID: func() string { setup: func(t *testing.T) testCase {
return "unknown" return testCase{
}, userID: "unknown",
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
wantErr: true, wantErr: true,
}
},
}, },
{ {
name: "created via grpc", name: "created via grpc",
setup: func(t *testing.T) testCase {
return testCase{
want: &resources.ScimUser{ want: &resources.ScimUser{
Name: &resources.ScimUserName{ Name: &resources.ScimUserName{
FamilyName: "Mouse", FamilyName: "Mouse",
@@ -80,21 +102,20 @@ func TestGetUser(t *testing.T) {
}, },
}, },
}, },
}
},
}, },
{ {
name: "created via scim", name: "created via scim",
buildUserID: func() string { setup: func(t *testing.T) testCase {
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) username := gofakeit.Username()
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err) require.NoError(t, err)
return createdUser.ID return testCase{
}, userID: createdUser.ID,
cleanup: func(userID string) {
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID})
require.NoError(t, err)
},
want: &resources.ScimUser{ want: &resources.ScimUser{
ExternalID: "701984", ExternalID: "701984",
UserName: "bjensen@example.com", UserName: username + "@example.com",
Name: &resources.ScimUserName{ Name: &resources.ScimUserName{
Formatted: "Babs Jensen", // DisplayName takes precedence Formatted: "Babs Jensen", // DisplayName takes precedence
FamilyName: "Jensen", FamilyName: "Jensen",
@@ -113,7 +134,7 @@ func TestGetUser(t *testing.T) {
Active: schemas.NewRelaxedBool(true), Active: schemas.NewRelaxedBool(true),
Emails: []*resources.ScimEmail{ Emails: []*resources.ScimEmail{
{ {
Value: "bjensen@example.com", Value: username + "@example.com",
Primary: true, Primary: true,
Type: "work", Type: "work",
}, },
@@ -194,80 +215,64 @@ func TestGetUser(t *testing.T) {
}, },
}, },
}, },
}
},
}, },
{ {
name: "scoped externalID", name: "scoped externalID",
buildUserID: func() string { setup: func(t *testing.T) testCase {
// create user without provisioning domain createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
require.NoError(t, err) require.NoError(t, err)
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
// set provisioning domain of service user require.NoError(t, err)
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") setProvisioningDomain(t, callingUserId, "fooBar")
// set externalID for provisioning domain
setAndEnsureMetadata(t, createdUser.ID, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") setAndEnsureMetadata(t, createdUser.ID, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId")
return createdUser.ID return testCase{
}, ctx: integration.WithAuthorizationToken(CTX, callingUserPat),
cleanup: func(userID string) { userID: createdUser.ID,
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID})
require.NoError(t, err)
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
},
want: &resources.ScimUser{ want: &resources.ScimUser{
ExternalID: "100-scopedExternalId", ExternalID: "100-scopedExternalId",
}, },
}
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ctx := tt.ctx ttt := tt.setup(t)
if ctx == nil { if ttt.userID == "" {
ctx = CTX ttt.userID = Instance.CreateHumanUser(CTX).UserId
} }
if ttt.ctx == nil {
var userID string ttt.ctx = CTX
if tt.buildUserID != nil {
userID = tt.buildUserID()
} else {
createUserResp := Instance.CreateHumanUser(CTX)
userID = createUserResp.UserId
} }
if ttt.orgID == "" {
orgID := tt.orgID ttt.orgID = Instance.DefaultOrg.Id
if orgID == "" {
orgID = Instance.DefaultOrg.Id
} }
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
var fetchedUser *resources.ScimUser require.EventuallyWithT(t, func(collect *assert.CollectT) {
var err error fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID)
require.EventuallyWithT(t, func(ttt *assert.CollectT) { if ttt.wantErr {
fetchedUser, err = Instance.Client.SCIM.Users.Get(ctx, orgID, userID) statusCode := ttt.errorStatus
if tt.wantErr {
statusCode := tt.errorStatus
if statusCode == 0 { if statusCode == 0 {
statusCode = http.StatusBadRequest statusCode = http.StatusBadRequest
} }
scim.RequireScimError(collect, statusCode, err)
scim.RequireScimError(ttt, statusCode, err)
return return
} }
if !assert.NoError(collect, err) {
assert.Equal(ttt, userID, fetchedUser.ID) scim.RequireScimError(collect, http.StatusNotFound, err)
assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas) return
assert.Equal(ttt, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType) }
assert.Equal(ttt, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location) assert.Equal(collect, ttt.userID, fetchedUser.ID)
assert.Nil(ttt, fetchedUser.Password) assert.EqualValues(collect, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas)
if !test.PartiallyDeepEqual(tt.want, fetchedUser) { assert.Equal(collect, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType)
ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want) assert.Equal(collect, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, ttt.orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location)
assert.Nil(collect, fetchedUser.Password)
if !test.PartiallyDeepEqual(ttt.want, fetchedUser) {
collect.Errorf("GetUser() got = %#v, want %#v", fetchedUser, ttt.want)
} }
}, retryDuration, tick) }, retryDuration, tick)
if tt.cleanup != nil {
tt.cleanup(fetchedUser.ID)
}
}) })
} }
} }

View File

@@ -23,15 +23,6 @@ var totalCountOfHumanUsers = 13
/* /*
func TestListUser(t *testing.T) { func TestListUser(t *testing.T) {
createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id)
defer func() {
// only the full user needs to be deleted, all others have random identification data
// fullUser is always the first one.
_, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{
UserId: createdUserIDs[0],
})
require.NoError(t, err)
}()
// secondary organization with same set of users, // secondary organization with same set of users,
// these should never be modified. // these should never be modified.
// This allows testing list requests without filters. // This allows testing list requests without filters.
@@ -451,7 +442,7 @@ func createUsers(t *testing.T, ctx context.Context, orgID string) []string {
// create the full scim user if on primary org // create the full scim user if on primary org
if orgID == Instance.DefaultOrg.Id { if orgID == Instance.DefaultOrg.Id {
fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, fullUserJson) fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err) require.NoError(t, err)
createdUserIDs = append(createdUserIDs, fullUserCreatedResp.ID) createdUserIDs = append(createdUserIDs, fullUserCreatedResp.ID)
count-- count--

View File

@@ -5,11 +5,13 @@ package integration_test
import ( import (
"context" "context"
_ "embed" _ "embed"
"fmt"
"net/http" "net/http"
"path" "path"
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/text/language" "golang.org/x/text/language"
@@ -20,7 +22,6 @@ import (
"github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/management"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
) )
var ( var (
@@ -199,28 +200,28 @@ func TestReplaceUser(t *testing.T) {
}, },
{ {
name: "not authenticated", name: "not authenticated",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: context.Background(), ctx: context.Background(),
wantErr: true, wantErr: true,
errorStatus: http.StatusUnauthorized, errorStatus: http.StatusUnauthorized,
}, },
{ {
name: "no permissions", name: "no permissions",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}, },
{ {
name: "another org", name: "another org",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
replaceUserOrgID: SecondaryOrganization.OrganizationId, replaceUserOrgID: SecondaryOrganization.OrganizationId,
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}, },
{ {
name: "another org with permissions", name: "another org with permissions",
body: minimalUserJson, body: withUsername(minimalUserJson, gofakeit.Username()),
replaceUserOrgID: SecondaryOrganization.OrganizationId, replaceUserOrgID: SecondaryOrganization.OrganizationId,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
wantErr: true, wantErr: true,
@@ -230,14 +231,9 @@ func TestReplaceUser(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// use iam owner => we don't want to test permissions of the create endpoint. // use iam owner => we don't want to test permissions of the create endpoint.
createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, fullUserJson) createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err) require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
assert.NoError(t, err)
}()
ctx := tt.ctx ctx := tt.ctx
if ctx == nil { if ctx == nil {
ctx = CTX ctx = CTX
@@ -294,10 +290,11 @@ func TestReplaceUser(t *testing.T) {
func TestReplaceUser_removeOldMetadata(t *testing.T) { func TestReplaceUser_removeOldMetadata(t *testing.T) {
// ensure old metadata is removed correctly // ensure old metadata is removed correctly
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) username := gofakeit.Username()
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err) require.NoError(t, err)
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserJson) _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserJson, username))
require.NoError(t, err) require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
@@ -312,20 +309,17 @@ func TestReplaceUser_removeOldMetadata(t *testing.T) {
for i := range md.Result { for i := range md.Result {
mdMap[md.Result[i].Key] = string(md.Result[i].Value) mdMap[md.Result[i].Key] = string(md.Result[i].Value)
} }
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true}]", username))
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1@example.com\",\"primary\":true}]")
}, retryDuration, tick) }, retryDuration, tick)
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
} }
func TestReplaceUser_emailType(t *testing.T) { func TestReplaceUser_emailType(t *testing.T) {
// ensure old metadata is removed correctly // ensure old metadata is removed correctly
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
require.NoError(t, err) require.NoError(t, err)
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithEmailTypeReplaceJson) replacedUsername := gofakeit.Username()
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserWithEmailTypeReplaceJson, replacedUsername))
require.NoError(t, err) require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
@@ -341,28 +335,26 @@ func TestReplaceUser_emailType(t *testing.T) {
mdMap[md.Result[i].Key] = string(md.Result[i].Value) mdMap[md.Result[i].Key] = string(md.Result[i].Value)
} }
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1-minimal-replaced@example.com\",\"primary\":true,\"type\":\"work\"}]") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true,\"type\":\"work\"}]", replacedUsername))
}, retryDuration, tick) }, retryDuration, tick)
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
} }
func TestReplaceUser_scopedExternalID(t *testing.T) { func TestReplaceUser_scopedExternalID(t *testing.T) {
// create user without provisioning domain set createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username()))
createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson)
require.NoError(t, err) require.NoError(t, err)
callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER")
require.NoError(t, err)
ctx := integration.WithAuthorizationToken(CTX, callingUserPat)
// set provisioning domain of service user // set provisioning domain of service user
setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBazz") setProvisioningDomain(t, callingUserId, "fooBazz")
// replace the user with provisioning domain set // replace the user with provisioning domain set
_, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson) _, err = Instance.Client.SCIM.Users.Replace(ctx, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson)
require.NoError(t, err) require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute)
require.EventuallyWithT(t, func(tt *assert.CollectT) { require.EventuallyWithT(t, func(tt *assert.CollectT) {
md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{ md, err := Instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{
Id: createdUser.ID, Id: createdUser.ID,
}) })
require.NoError(tt, err) require.NoError(tt, err)
@@ -376,9 +368,4 @@ func TestReplaceUser_scopedExternalID(t *testing.T) {
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:externalId", "701984") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:externalId", "701984")
test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:fooBazz:externalId", "replaced-external-id") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:fooBazz:externalId", "replaced-external-id")
}, retryDuration, tick) }, retryDuration, tick)
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID})
require.NoError(t, err)
removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID)
} }

View File

@@ -5,11 +5,13 @@ package integration_test
import ( import (
"context" "context"
_ "embed" _ "embed"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"testing" "testing"
"time" "time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/text/language" "golang.org/x/text/language"
@@ -19,7 +21,6 @@ import (
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/integration/scim"
"github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/internal/test"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
) )
var ( var (
@@ -34,15 +35,7 @@ func init() {
} }
func TestUpdateUser(t *testing.T) { func TestUpdateUser(t *testing.T) {
fullUserCreated, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) type testCase struct {
require.NoError(t, err)
defer func() {
_, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: fullUserCreated.ID})
require.NoError(t, err)
}()
tests := []struct {
name string name string
body []byte body []byte
ctx context.Context ctx context.Context
@@ -52,79 +45,165 @@ func TestUpdateUser(t *testing.T) {
wantErr bool wantErr bool
scimErrorType string scimErrorType string
errorStatus int errorStatus int
}
tests := []struct {
name string
setup func(t *testing.T) testCase
}{ }{
{ {
name: "not authenticated", name: "not authenticated",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
ctx: context.Background(), ctx: context.Background(),
body: minimalUserUpdateJson, body: minimalUserUpdateJson,
wantErr: true, wantErr: true,
errorStatus: http.StatusUnauthorized, errorStatus: http.StatusUnauthorized,
}
},
}, },
{ {
name: "no permissions", name: "no permissions",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission),
body: minimalUserUpdateJson, body: minimalUserUpdateJson,
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}
},
}, },
{ {
name: "other org", name: "other org",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
orgID: SecondaryOrganization.OrganizationId, orgID: SecondaryOrganization.OrganizationId,
body: minimalUserUpdateJson, body: minimalUserUpdateJson,
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}
},
}, },
{ {
name: "other org with permissions", name: "other org with permissions",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner),
orgID: SecondaryOrganization.OrganizationId, orgID: SecondaryOrganization.OrganizationId,
body: minimalUserUpdateJson, body: minimalUserUpdateJson,
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}
},
}, },
{ {
name: "invalid patch json", name: "invalid patch json",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("nickname", "10"), body: simpleReplacePatchBody("nickname", "10"),
wantErr: true, wantErr: true,
scimErrorType: "invalidValue", scimErrorType: "invalidValue",
}
},
}, },
{ {
name: "password complexity violation", name: "password complexity violation",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("password", `"fooBar"`), body: simpleReplacePatchBody("password", `"fooBar"`),
wantErr: true, wantErr: true,
scimErrorType: "invalidValue", scimErrorType: "invalidValue",
}
},
}, },
{ {
name: "invalid profile url", name: "invalid profile url",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`), body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`),
wantErr: true, wantErr: true,
scimErrorType: "invalidValue", scimErrorType: "invalidValue",
}
},
}, },
{ {
name: "invalid time zone", name: "invalid time zone",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("timezone", `"foobar"`), body: simpleReplacePatchBody("timezone", `"foobar"`),
wantErr: true, wantErr: true,
scimErrorType: "invalidValue", scimErrorType: "invalidValue",
}
},
}, },
{ {
name: "invalid locale", name: "invalid locale",
setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: simpleReplacePatchBody("locale", `"foobar"`), body: simpleReplacePatchBody("locale", `"foobar"`),
wantErr: true, wantErr: true,
scimErrorType: "invalidValue", scimErrorType: "invalidValue",
}
},
}, },
{ {
name: "unknown user id", name: "unknown user id",
setup: func(t *testing.T) testCase {
return testCase{
body: simpleReplacePatchBody("nickname", `"foo"`), body: simpleReplacePatchBody("nickname", `"foo"`),
userID: "fooBar", userID: "fooBar",
wantErr: true, wantErr: true,
errorStatus: http.StatusNotFound, errorStatus: http.StatusNotFound,
}
},
}, },
{ {
name: "full", name: "full",
body: fullUserUpdateJson, setup: func(t *testing.T) testCase {
username := gofakeit.Username()
created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username))
require.NoError(t, err)
return testCase{
userID: created.ID,
body: withUsername(fullUserUpdateJson, username),
want: &resources.ScimUser{ want: &resources.ScimUser{
ExternalID: "fooBAR", ExternalID: "fooBAR",
UserName: "bjensen@example.com", UserName: username + "@example.com",
Name: &resources.ScimUserName{ Name: &resources.ScimUserName{
Formatted: "replaced-display-name", Formatted: "replaced-display-name",
FamilyName: "added-family-name", FamilyName: "added-family-name",
@@ -138,15 +217,15 @@ func TestUpdateUser(t *testing.T) {
ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")),
Emails: []*resources.ScimEmail{ Emails: []*resources.ScimEmail{
{ {
Value: "bjensen@example.com", Value: username + "@example.com",
Type: "work", Type: "work",
}, },
{ {
Value: "babs@jensen.org", Value: username + "+1@example.com",
Type: "home", Type: "home",
}, },
{ {
Value: "babs@example.com", Value: username + "+2@example.com",
Primary: true, Primary: true,
Type: "home", Type: "home",
}, },
@@ -213,54 +292,49 @@ func TestUpdateUser(t *testing.T) {
}, },
}, },
Title: "Tour Guide", Title: "Tour Guide",
PreferredLanguage: language.MustParse("en-US"), PreferredLanguage: language.MustParse("en"),
Locale: "en-US", Locale: "en-US",
Timezone: "America/Los_Angeles", Timezone: "America/Los_Angeles",
Active: schemas.NewRelaxedBool(true), Active: schemas.NewRelaxedBool(true),
}, },
}
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if tt.ctx == nil { ttt := tt.setup(t)
tt.ctx = CTX if ttt.orgID == "" {
ttt.orgID = Instance.DefaultOrg.Id
} }
if ttt.ctx == nil {
if tt.orgID == "" { ttt.ctx = CTX
tt.orgID = Instance.DefaultOrg.Id
} }
err := Instance.Client.SCIM.Users.Update(ttt.ctx, ttt.orgID, ttt.userID, ttt.body)
if tt.userID == "" { if ttt.wantErr {
tt.userID = fullUserCreated.ID
}
err := Instance.Client.SCIM.Users.Update(tt.ctx, tt.orgID, tt.userID, tt.body)
if tt.wantErr {
require.Error(t, err) require.Error(t, err)
statusCode := ttt.errorStatus
statusCode := tt.errorStatus
if statusCode == 0 { if statusCode == 0 {
statusCode = http.StatusBadRequest statusCode = http.StatusBadRequest
} }
scimErr := scim.RequireScimError(t, statusCode, err) scimErr := scim.RequireScimError(t, statusCode, err)
assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType) assert.Equal(t, ttt.scimErrorType, scimErr.Error.ScimType)
return return
} }
require.NoError(t, err) require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 3*time.Minute)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) {
require.EventuallyWithT(t, func(ttt *assert.CollectT) { fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID)
fetchedUser, err := Instance.Client.SCIM.Users.Get(tt.ctx, tt.orgID, fullUserCreated.ID) require.NoError(collect, err)
require.NoError(ttt, err)
fetchedUser.Resource = nil fetchedUser.Resource = nil
fetchedUser.ID = "" fetchedUser.ID = ""
if tt.want != nil && !test.PartiallyDeepEqual(tt.want, fetchedUser) { fetched, err := json.Marshal(fetchedUser)
ttt.Errorf("got = %#v, want = %#v", fetchedUser, tt.want) require.NoError(collect, err)
} want, err := json.Marshal(ttt.want)
require.NoError(collect, err)
assert.JSONEq(collect, string(want), string(fetched))
}, retryDuration, tick) }, retryDuration, tick)
}) })
} }