diff --git a/go.mod b/go.mod index 78ee24a..cfb8dce 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 go.infratographer.com/x v0.3.3 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 @@ -22,6 +23,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/garsue/watermillzap v1.2.0 // indirect github.com/go-logr/logr v1.2.4 // indirect @@ -52,6 +54,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.40.0 // indirect @@ -59,6 +62,7 @@ require ( github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index 2b2765e..b7826d7 100644 --- a/go.sum +++ b/go.sum @@ -280,6 +280,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= diff --git a/internal/metal/providers/mock.go b/internal/metal/providers/mock.go new file mode 100644 index 0000000..91ee7a9 --- /dev/null +++ b/internal/metal/providers/mock.go @@ -0,0 +1,52 @@ +package providers + +import ( + "context" + + "github.com/stretchr/testify/mock" + "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" +) + +var _ Provider = &MockProvider{} + +// MockProvider implements Provider used for testing. +type MockProvider struct { + mock.Mock +} + +// GetOrganizationDetails implements Provider used for testing. +func (p *MockProvider) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { + args := p.Called(id) + + return args.Get(0).(*models.OrganizationDetails), args.Error(1) +} + +// GetProjectDetails implements Provider used for testing. +func (p *MockProvider) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { + args := p.Called(id) + + return args.Get(0).(*models.ProjectDetails), args.Error(1) +} + +// GetUserDetails implements Provider used for testing. +func (p *MockProvider) GetUserDetails(ctx context.Context, id gidx.PrefixedID) (*models.UserDetails, error) { + args := p.Called(id) + + return args.Get(0).(*models.UserDetails), args.Error(1) +} + +// GetUserOrganizationRole implements Provider used for testing. +func (p *MockProvider) GetUserOrganizationRole(ctx context.Context, userID, orgID gidx.PrefixedID) (string, error) { + args := p.Called(userID, orgID) + + return args.String(0), args.Error(1) +} + +// GetUserProjectRole implements Provider used for testing. +func (p *MockProvider) GetUserProjectRole(ctx context.Context, userID, projID gidx.PrefixedID) (string, error) { + args := p.Called(userID, projID) + + return args.String(0), args.Error(1) +} diff --git a/internal/permissions/mock.go b/internal/permissions/mock.go new file mode 100644 index 0000000..8dac744 --- /dev/null +++ b/internal/permissions/mock.go @@ -0,0 +1,90 @@ +package permissions + +import ( + "context" + + "github.com/stretchr/testify/mock" + "go.infratographer.com/x/gidx" +) + +// MockClient implements Client for testing. +type MockClient struct { + mock.Mock +} + +// AssignRole implements Client for testing. +func (c *MockClient) AssignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { + args := c.Called(roleID, memberID) + + return args.Error(0) +} + +// CreateRole implements Client for testing. +func (c *MockClient) CreateRole(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (gidx.PrefixedID, error) { + args := c.Called(resourceID, actions) + + return args.Get(0).(gidx.PrefixedID), args.Error(1) +} + +// DeleteResourceRelationship implements Client for testing. +func (c *MockClient) DeleteResourceRelationship(ctx context.Context, resourceID gidx.PrefixedID, relation string, relatedResourceID gidx.PrefixedID) error { + args := c.Called(resourceID, relation, relatedResourceID) + + return args.Error(0) +} + +// DeleteRole implements Client for testing. +func (c *MockClient) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) error { + args := c.Called(roleID) + + return args.Error(0) +} + +// FindResourceRoleByActions implements Client for testing. +func (c *MockClient) FindResourceRoleByActions(ctx context.Context, resourceID gidx.PrefixedID, actions []string) (ResourceRole, error) { + args := c.Called(resourceID, actions) + + return args.Get(0).(ResourceRole), args.Error(1) +} + +// ListResourceRelationshipsFrom implements Client for testing. +func (c *MockClient) ListResourceRelationshipsFrom(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) { + args := c.Called(resourceID) + + return args.Get(0).([]ResourceRelationship), args.Error(1) +} + +// ListResourceRelationshipsTo implements Client for testing. +func (c *MockClient) ListResourceRelationshipsTo(ctx context.Context, resourceID gidx.PrefixedID) ([]ResourceRelationship, error) { + args := c.Called(resourceID) + + return args.Get(0).([]ResourceRelationship), args.Error(1) +} + +// ListResourceRoles implements Client for testing. +func (c *MockClient) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) (ResourceRoles, error) { + args := c.Called(resourceID) + + return args.Get(0).(ResourceRoles), args.Error(1) +} + +// ListRoleAssignments implements Client for testing. +func (c *MockClient) ListRoleAssignments(ctx context.Context, roleID gidx.PrefixedID) ([]gidx.PrefixedID, error) { + args := c.Called(roleID) + + return args.Get(0).([]gidx.PrefixedID), args.Error(1) +} + +// RoleHasAssignment implements Client for testing. +func (c *MockClient) RoleHasAssignment(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) (bool, error) { + args := c.Called(roleID, memberID) + + return args.Bool(0), args.Error(1) +} + +// UnassignRole implements Client for testing. +func (c *MockClient) UnassignRole(ctx context.Context, roleID gidx.PrefixedID, memberID gidx.PrefixedID) error { + args := c.Called(roleID, memberID) + + return args.Error(0) +} diff --git a/internal/service/organizations_test.go b/internal/service/organizations_test.go new file mode 100644 index 0000000..ead7efa --- /dev/null +++ b/internal/service/organizations_test.go @@ -0,0 +1,357 @@ +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" + "go.equinixmetal.net/infra9-metal-bridge/internal/service" +) + +type mockPublisher struct { + mock.Mock +} + +func (p *mockPublisher) PublishChange(ctx context.Context, subjectType string, change events.ChangeMessage) error { + args := p.Called(subjectType, change) + + return args.Error(0) +} + +func TestTouchOrganizationEmpty(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + Projects: []*models.ProjectDetails{ + project, + }, + Memberships: []*models.Membership[models.OrganizationDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetOrganizationDetails", orgID).Return(org, nil) + + mPerms.On("ListResourceRelationshipsFrom", orgID).Return([]permissions.ResourceRelationship{}, nil) + mPerms.On("ListResourceRelationshipsTo", orgID).Return([]permissions.ResourceRelationship{}, nil) + + orgTenantChangeMessage := events.ChangeMessage{ + SubjectID: orgID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + rootTenantID, + }, + } + relateProjectChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalorganization", orgTenantChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metalorganization", relateProjectChangeMessage).Return(nil) + + // Memberships + + newRoleID := gidx.PrefixedID("permrol-role1") + + mPerms.On("ListResourceRoles", orgID).Return(permissions.ResourceRoles{}, nil) + mPerms.On("CreateRole", orgID, roleMap["owner"]).Return(newRoleID, nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchOrganization(context.Background(), orgID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchOrganizationCleanup(t *testing.T) { + oldRootTenantID := gidx.PrefixedID("tnntten-root1") + newRootTenantID := gidx.PrefixedID("tnntten-root2") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + "collaborator": { + "action1", + }, + } + + oldUserID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + deadProjectID := gidx.PrefixedID("metlprj-prj1") + + userID := gidx.PrefixedID("idntusr-RnKvdujrwqm4o1dBDgfgaqeCpKFMaGeOtGnNbZky0Kg") + user := &models.UserDetails{ + ID: "usr2", + } + + projectID := gidx.PrefixedID("metlprj-prj2") + project := &models.ProjectDetails{ + ID: "prj2", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + Projects: []*models.ProjectDetails{ + project, + }, + Memberships: []*models.Membership[models.OrganizationDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetOrganizationDetails", orgID).Return(org, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: oldRootTenantID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", orgID).Return(existingRelsFrom, nil) + + existingRelsTo := []permissions.ResourceRelationship{ + { + ResourceID: deadProjectID, + Relation: string(service.RelateParent), + }, + } + + mPerms.On("ListResourceRelationshipsTo", orgID).Return(existingRelsTo, nil) + + mPerms.On("DeleteResourceRelationship", orgID, string(service.RelateParent), oldRootTenantID).Return(nil) + mPerms.On("DeleteResourceRelationship", deadProjectID, string(service.RelateParent), orgID).Return(nil) + + orgTenantChangeMessage := events.ChangeMessage{ + SubjectID: orgID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + newRootTenantID, + }, + } + relateProjectChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalorganization", orgTenantChangeMessage).Return(nil) + mPublisher.On("PublishChange", "metalorganization", relateProjectChangeMessage).Return(nil) + + // Memberships + + oldRoleID := gidx.PrefixedID("permrol-role1") + newRoleID := gidx.PrefixedID("permrol-role2") + + existingRoles := permissions.ResourceRoles{ + { + ID: oldRoleID, + Actions: roleMap["collaborator"], + }, + } + + mPerms.On("ListResourceRoles", orgID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + oldUserID, + } + + mPerms.On("ListRoleAssignments", oldRoleID).Return(existingRoleAssignments, nil) + + mPerms.On("CreateRole", orgID, roleMap["owner"]).Return(newRoleID, nil) + mPerms.On("DeleteRole", oldRoleID).Return(nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + mPerms.On("UnassignRole", oldRoleID, oldUserID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(newRootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchOrganization(context.Background(), orgID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchOrganizationNoChange(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + Projects: []*models.ProjectDetails{ + project, + }, + Memberships: []*models.Membership[models.OrganizationDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetOrganizationDetails", orgID).Return(org, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: rootTenantID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", orgID).Return(existingRelsFrom, nil) + + existingRelsTo := []permissions.ResourceRelationship{ + { + ResourceID: projectID, + Relation: string(service.RelateParent), + }, + } + + mPerms.On("ListResourceRelationshipsTo", orgID).Return(existingRelsTo, nil) + + // Memberships + + roleID := gidx.PrefixedID("permrol-role1") + + existingRoles := permissions.ResourceRoles{ + { + ID: roleID, + Actions: roleMap["owner"], + }, + } + + mPerms.On("ListResourceRoles", orgID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + userID, + } + + mPerms.On("ListRoleAssignments", roleID).Return(existingRoleAssignments, nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchOrganization(context.Background(), orgID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} diff --git a/internal/service/projects_test.go b/internal/service/projects_test.go new file mode 100644 index 0000000..e3de5ec --- /dev/null +++ b/internal/service/projects_test.go @@ -0,0 +1,303 @@ +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.infratographer.com/x/events" + "go.infratographer.com/x/gidx" + + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" + "go.equinixmetal.net/infra9-metal-bridge/internal/metal/providers" + "go.equinixmetal.net/infra9-metal-bridge/internal/permissions" + "go.equinixmetal.net/infra9-metal-bridge/internal/service" +) + +func TestTouchProjectEmpty(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + Organization: org, + Memberships: []*models.Membership[models.ProjectDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetProjectDetails", projectID).Return(project, nil) + + mPerms.On("ListResourceRelationshipsFrom", projectID).Return([]permissions.ResourceRelationship{}, nil) + + relateParentChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalproject", relateParentChangeMessage).Return(nil) + + // Memberships + + newRoleID := gidx.PrefixedID("permrol-role1") + + mPerms.On("ListResourceRoles", projectID).Return(permissions.ResourceRoles{}, nil) + mPerms.On("CreateRole", projectID, roleMap["owner"]).Return(newRoleID, nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchProject(context.Background(), projectID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchProjectCleanup(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + "collaborator": { + "action1", + }, + } + + oldUserID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + oldOrgID := gidx.PrefixedID("metlorg-org1") + + userID := gidx.PrefixedID("idntusr-RnKvdujrwqm4o1dBDgfgaqeCpKFMaGeOtGnNbZky0Kg") + user := &models.UserDetails{ + ID: "usr2", + } + + orgID := gidx.PrefixedID("metlorg-org2") + org := &models.OrganizationDetails{ + ID: "org2", + } + + projectID := gidx.PrefixedID("metlprj-prj2") + project := &models.ProjectDetails{ + ID: "prj2", + Organization: org, + Memberships: []*models.Membership[models.ProjectDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetProjectDetails", projectID).Return(project, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: oldOrgID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", projectID).Return(existingRelsFrom, nil) + + mPerms.On("DeleteResourceRelationship", projectID, string(service.RelateParent), oldOrgID).Return(nil) + + relateParentChangeMessage := events.ChangeMessage{ + SubjectID: projectID, + EventType: string(events.CreateChangeType), + AdditionalSubjectIDs: []gidx.PrefixedID{ + orgID, + }, + } + + mPublisher.On("PublishChange", "metalproject", relateParentChangeMessage).Return(nil) + + // Memberships + + oldRoleID := gidx.PrefixedID("permrol-role1") + newRoleID := gidx.PrefixedID("permrol-role2") + + existingRoles := permissions.ResourceRoles{ + { + ID: oldRoleID, + Actions: roleMap["collaborator"], + }, + } + + mPerms.On("ListResourceRoles", projectID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + oldUserID, + } + + mPerms.On("ListRoleAssignments", oldRoleID).Return(existingRoleAssignments, nil) + + mPerms.On("CreateRole", projectID, roleMap["owner"]).Return(newRoleID, nil) + mPerms.On("DeleteRole", oldRoleID).Return(nil) + + mPerms.On("AssignRole", newRoleID, userID).Return(nil) + mPerms.On("UnassignRole", oldRoleID, oldUserID).Return(nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchProject(context.Background(), projectID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +} + +func TestTouchProjectNoChange(t *testing.T) { + rootTenantID := gidx.PrefixedID("tnntten-root1") + + roleMap := map[string][]string{ + "owner": { + "action1", + "action2", + }, + } + + userID := gidx.PrefixedID("idntusr-2s-9kVNPJBaInlHpRs3lAMsvU_kVkLaSlD4R_RhavDw") + user := &models.UserDetails{ + ID: "usr1", + } + + orgID := gidx.PrefixedID("metlorg-org1") + org := &models.OrganizationDetails{ + ID: "org1", + } + + projectID := gidx.PrefixedID("metlprj-prj1") + project := &models.ProjectDetails{ + ID: "prj1", + Organization: org, + Memberships: []*models.Membership[models.ProjectDetails]{ + { + User: user, + Roles: []string{ + "owner", + }, + }, + }, + } + + var ( + mMetal = new(providers.MockProvider) + mPerms = new(permissions.MockClient) + mPublisher = new(mockPublisher) + ) + + // Relationships + + mMetal.On("GetProjectDetails", projectID).Return(project, nil) + + existingRelsFrom := []permissions.ResourceRelationship{ + { + Relation: string(service.RelateParent), + SubjectID: orgID, + }, + } + + mPerms.On("ListResourceRelationshipsFrom", projectID).Return(existingRelsFrom, nil) + + // Memberships + + roleID := gidx.PrefixedID("permrol-role1") + + existingRoles := permissions.ResourceRoles{ + { + ID: roleID, + Actions: roleMap["owner"], + }, + } + + mPerms.On("ListResourceRoles", projectID).Return(existingRoles, nil) + + existingRoleAssignments := []gidx.PrefixedID{ + userID, + } + + mPerms.On("ListRoleAssignments", roleID).Return(existingRoleAssignments, nil) + + // Run scenario + + svc, err := service.New(mPublisher, mMetal, mPerms, + service.WithRootTenant(rootTenantID.String()), + service.WithRoles(roleMap), + ) + + require.NoError(t, err) + + err = svc.TouchProject(context.Background(), projectID) + + require.NoError(t, err) + + require.True(t, mMetal.AssertExpectations(t), "unexpected calls to metal provider") + require.True(t, mPerms.AssertExpectations(t), "unexpected calls to permissions client") + require.True(t, mPublisher.AssertExpectations(t), "unexpected calls to events publisher") +}