From ab5fe8e5e2cd54125169d6ffc70f237bf1a65104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 18 Feb 2026 12:05:53 +0400 Subject: [PATCH] Fix e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- e2e/console/asset_test.go | 29 +- e2e/console/continual_improvement_test.go | 28 +- e2e/console/datum_test.go | 130 +++--- e2e/console/document_test.go | 106 ++--- e2e/console/document_version_test.go | 17 +- e2e/console/mapping_test.go | 13 +- e2e/console/meeting_test.go | 6 +- e2e/console/nonconformity_test.go | 21 +- e2e/console/obligation_test.go | 21 +- e2e/console/rbac_test.go | 18 +- e2e/console/risk_test.go | 6 +- e2e/console/task_assignment_test.go | 10 +- e2e/console/task_test.go | 6 +- e2e/console/testdata/config.yaml | 2 +- e2e/console/{member_test.go => user_test.go} | 182 +++----- e2e/console/vendor_test.go | 12 +- e2e/internal/factory/factory.go | 47 ++ e2e/internal/testutil/client.go | 171 ++++--- e2e/internal/testutil/mailpit.go | 101 ++++ e2e/internal/testutil/testutil.go | 28 +- packages/emails/emails.go | 1 - pkg/coredata/membership_profile.go | 10 + pkg/coredata/migrations/20260212T163603Z.sql | 23 +- pkg/iam/iam_actions.go | 12 +- pkg/iam/iam_policies.go | 4 + pkg/iam/organization_service.go | 39 +- pkg/server/api/connect/v1/schema.graphql | 37 +- pkg/server/api/connect/v1/schema/schema.go | 459 +++++++++---------- pkg/server/api/connect/v1/types/types.go | 24 +- pkg/server/api/connect/v1/v1_resolver.go | 26 +- 30 files changed, 874 insertions(+), 715 deletions(-) rename e2e/console/{member_test.go => user_test.go} (52%) create mode 100644 e2e/internal/testutil/mailpit.go diff --git a/e2e/console/asset_test.go b/e2e/console/asset_test.go index 68c45a5f5..eb780d7a4 100644 --- a/e2e/console/asset_test.go +++ b/e2e/console/asset_test.go @@ -20,15 +20,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) func TestAsset_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) const query = ` mutation($input: CreateAssetInput!) { @@ -71,7 +70,7 @@ func TestAsset_Create(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "name": "Production Database Server", "amount": 5, - "ownerId": profileID.String(), + "ownerId": profileID, "assetType": "VIRTUAL", "dataTypesStored": "Customer PII, Financial Records", }, @@ -84,15 +83,14 @@ func TestAsset_Create(t *testing.T) { assert.Equal(t, 5, asset.Amount) assert.Equal(t, "VIRTUAL", asset.AssetType) assert.Equal(t, "Customer PII, Financial Records", asset.DataTypesStored) - assert.Equal(t, profileID.String(), asset.Owner.ID) + assert.Equal(t, profileID, asset.Owner.ID) } func TestAsset_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) const createQuery = ` mutation($input: CreateAssetInput!) { @@ -121,7 +119,7 @@ func TestAsset_Update(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "name": "Test Asset", "amount": 10, - "ownerId": profileID.String(), + "ownerId": profileID, "assetType": "VIRTUAL", "dataTypesStored": "Test data", }, @@ -172,8 +170,7 @@ func TestAsset_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) const createQuery = ` mutation($input: CreateAssetInput!) { @@ -202,7 +199,7 @@ func TestAsset_Delete(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "name": "Asset to delete", "amount": 1, - "ownerId": profileID.String(), + "ownerId": profileID, "assetType": "VIRTUAL", "dataTypesStored": "None", }, @@ -237,8 +234,7 @@ func TestAsset_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) // Create multiple assets for i := 0; i < 3; i++ { @@ -269,7 +265,7 @@ func TestAsset_List(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "name": fmt.Sprintf("Asset %c", 'A'+i), "amount": i + 1, - "ownerId": profileID.String(), + "ownerId": profileID, "assetType": "VIRTUAL", "dataTypesStored": "Test data", }, @@ -324,8 +320,7 @@ func TestAsset_Types(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) assetTypes := []string{"PHYSICAL", "VIRTUAL"} @@ -360,7 +355,7 @@ func TestAsset_Types(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "name": "Asset " + assetType, "amount": 1, - "ownerId": profileID.String(), + "ownerId": profileID, "assetType": assetType, "dataTypesStored": "Test data", }, diff --git a/e2e/console/continual_improvement_test.go b/e2e/console/continual_improvement_test.go index 9a6e2e7f7..95cf36a79 100644 --- a/e2e/console/continual_improvement_test.go +++ b/e2e/console/continual_improvement_test.go @@ -21,14 +21,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) func TestContinualImprovement_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) query := ` mutation CreateContinualImprovement($input: CreateContinualImprovementInput!) { @@ -68,7 +68,7 @@ func TestContinualImprovement_Create(t *testing.T) { "referenceId": fmt.Sprintf("CI-%d", time.Now().UnixNano()), "description": "Improve security training program", "source": "Internal Audit", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", "priority": "HIGH", }, @@ -86,8 +86,7 @@ func TestContinualImprovement_Create(t *testing.T) { func TestContinualImprovement_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) createQuery := ` mutation CreateContinualImprovement($input: CreateContinualImprovementInput!) { @@ -116,7 +115,7 @@ func TestContinualImprovement_Update(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "referenceId": fmt.Sprintf("CI-UPDATE-%d", time.Now().UnixNano()), "description": "Original description", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", "priority": "LOW", }, @@ -167,8 +166,7 @@ func TestContinualImprovement_Update(t *testing.T) { func TestContinualImprovement_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) createQuery := ` mutation CreateContinualImprovement($input: CreateContinualImprovementInput!) { @@ -196,7 +194,7 @@ func TestContinualImprovement_Delete(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "referenceId": fmt.Sprintf("CI-DELETE-%d", time.Now().UnixNano()), - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", "priority": "LOW", }, @@ -230,8 +228,7 @@ func TestContinualImprovement_Delete(t *testing.T) { func TestContinualImprovement_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) createQuery := ` mutation CreateContinualImprovement($input: CreateContinualImprovementInput!) { @@ -251,7 +248,7 @@ func TestContinualImprovement_List(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "referenceId": fmt.Sprintf("CI-LIST-%d-%d", i, time.Now().UnixNano()), "description": fmt.Sprintf("Improvement %d", i), - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", "priority": "MEDIUM", }, @@ -305,8 +302,7 @@ func TestContinualImprovement_List(t *testing.T) { func TestContinualImprovement_StatusAndPriorityValues(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) t.Run("status values", func(t *testing.T) { statuses := []string{"OPEN", "IN_PROGRESS", "CLOSED"} @@ -341,7 +337,7 @@ func TestContinualImprovement_StatusAndPriorityValues(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "referenceId": fmt.Sprintf("CI-STATUS-%s-%d", status, time.Now().UnixNano()), - "ownerId": profileID.String(), + "ownerId": profileID, "status": status, "priority": "LOW", }, @@ -385,7 +381,7 @@ func TestContinualImprovement_StatusAndPriorityValues(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "referenceId": fmt.Sprintf("CI-PRIORITY-%s-%d", priority, time.Now().UnixNano()), - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", "priority": priority, }, diff --git a/e2e/console/datum_test.go b/e2e/console/datum_test.go index 2bc9c3a52..2ee98c242 100644 --- a/e2e/console/datum_test.go +++ b/e2e/console/datum_test.go @@ -29,8 +29,7 @@ import ( func TestDatum_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) tests := []struct { name string @@ -103,7 +102,7 @@ func TestDatum_Create(t *testing.T) { input := map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "ownerId": profileID.String(), + "ownerId": profileID, } for k, v := range tt.input { input[k] = v @@ -140,8 +139,7 @@ func TestDatum_Create(t *testing.T) { func TestDatum_Create_Validation(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) tests := []struct { name string @@ -277,8 +275,7 @@ func TestDatum_Create_Validation(t *testing.T) { func TestDatum_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) tests := []struct { name string @@ -290,7 +287,7 @@ func TestDatum_Update(t *testing.T) { { name: "update name", setup: func() string { - return factory.NewDatum(owner, profileID.String()). + return factory.NewDatum(owner, profileID). WithName("Datum to Update"). Create() }, @@ -306,7 +303,7 @@ func TestDatum_Update(t *testing.T) { { name: "update to PUBLIC classification", setup: func() string { - return factory.NewDatum(owner, profileID.String()). + return factory.NewDatum(owner, profileID). WithName("Classification Test"). WithDataClassification("INTERNAL"). Create() @@ -320,7 +317,7 @@ func TestDatum_Update(t *testing.T) { { name: "update to SECRET classification", setup: func() string { - return factory.NewDatum(owner, profileID.String()). + return factory.NewDatum(owner, profileID). WithName("Classification Test"). Create() }, @@ -375,9 +372,8 @@ func TestDatum_Update(t *testing.T) { func TestDatum_Update_Validation(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - baseDatumID := factory.NewDatum(owner, profileID.String()).WithName("Validation Test Datum").Create() + profileID := factory.CreateUser(owner) + baseDatumID := factory.NewDatum(owner, profileID).WithName("Validation Test Datum").Create() tests := []struct { name string @@ -491,11 +487,10 @@ func TestDatum_Update_Validation(t *testing.T) { func TestDatum_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) t.Run("delete existing datum", func(t *testing.T) { - datumID := factory.NewDatum(owner, profileID.String()).WithName("Datum to Delete").Create() + datumID := factory.NewDatum(owner, profileID).WithName("Datum to Delete").Create() query := ` mutation DeleteDatum($input: DeleteDatumInput!) { @@ -557,12 +552,11 @@ func TestDatum_Delete_Validation(t *testing.T) { func TestDatum_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) datumNames := []string{"Datum A", "Datum B", "Datum C"} for _, name := range datumNames { - factory.NewDatum(owner, profileID.String()).WithName(name).Create() + factory.NewDatum(owner, profileID).WithName(name).Create() } query := ` @@ -630,8 +624,7 @@ func TestDatum_Query(t *testing.T) { func TestDatum_Timestamps(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) t.Run("createdAt and updatedAt are set on create", func(t *testing.T) { beforeCreate := time.Now().Add(-time.Second) @@ -665,7 +658,7 @@ func TestDatum_Timestamps(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "ownerId": profileID.String(), + "ownerId": profileID, "name": "Timestamp Test Datum", "dataClassification": "INTERNAL", }, @@ -677,7 +670,7 @@ func TestDatum_Timestamps(t *testing.T) { }) t.Run("updatedAt changes on update", func(t *testing.T) { - datumID := factory.NewDatum(owner, profileID.String()).WithName("Timestamp Update Test").Create() + datumID := factory.NewDatum(owner, profileID).WithName("Timestamp Update Test").Create() getQuery := ` query($id: ID!) { @@ -741,9 +734,8 @@ func TestDatum_Timestamps(t *testing.T) { func TestDatum_SubResolvers(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("SubResolver Test Datum").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("SubResolver Test Datum").Create() t.Run("owner sub-resolver", func(t *testing.T) { query := ` @@ -772,7 +764,7 @@ func TestDatum_SubResolvers(t *testing.T) { err := owner.Execute(query, map[string]any{"id": datumID}, &result) require.NoError(t, err) - assert.Equal(t, profileID.String(), result.Node.Owner.ID) + assert.Equal(t, profileID, result.Node.Owner.ID) }) t.Run("organization sub-resolver", func(t *testing.T) { @@ -813,8 +805,7 @@ func TestDatum_RBAC(t *testing.T) { t.Run("create", func(t *testing.T) { t.Run("owner can create", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) _, err := owner.Do(` mutation CreateDatum($input: CreateDatumInput!) { @@ -825,7 +816,7 @@ func TestDatum_RBAC(t *testing.T) { `, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "ownerId": profileID.String(), + "ownerId": profileID, "name": "RBAC Test Datum", "dataClassification": "INTERNAL", }, @@ -836,8 +827,7 @@ func TestDatum_RBAC(t *testing.T) { t.Run("admin can create", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) _, err := admin.Do(` mutation CreateDatum($input: CreateDatumInput!) { @@ -848,7 +838,7 @@ func TestDatum_RBAC(t *testing.T) { `, map[string]any{ "input": map[string]any{ "organizationId": admin.GetOrganizationID().String(), - "ownerId": profileID.String(), + "ownerId": profileID, "name": "RBAC Test Datum", "dataClassification": "INTERNAL", }, @@ -859,8 +849,7 @@ func TestDatum_RBAC(t *testing.T) { t.Run("viewer cannot create", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) _, err := viewer.Do(` mutation CreateDatum($input: CreateDatumInput!) { @@ -871,7 +860,7 @@ func TestDatum_RBAC(t *testing.T) { `, map[string]any{ "input": map[string]any{ "organizationId": viewer.GetOrganizationID().String(), - "ownerId": profileID.String(), + "ownerId": profileID, "name": "RBAC Test Datum", "dataClassification": "INTERNAL", }, @@ -883,9 +872,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("update", func(t *testing.T) { t.Run("owner can update", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Update Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Update Test").Create() _, err := owner.Do(` mutation UpdateDatum($input: UpdateDatumInput!) { @@ -905,9 +893,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("admin can update", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Update Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Update Test").Create() _, err := admin.Do(` mutation UpdateDatum($input: UpdateDatumInput!) { @@ -927,9 +914,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("viewer cannot update", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Update Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Update Test").Create() _, err := viewer.Do(` mutation UpdateDatum($input: UpdateDatumInput!) { @@ -950,9 +936,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("delete", func(t *testing.T) { t.Run("owner can delete", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Delete Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Delete Test").Create() _, err := owner.Do(` mutation DeleteDatum($input: DeleteDatumInput!) { @@ -969,9 +954,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("admin can delete", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Delete Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Delete Test").Create() _, err := admin.Do(` mutation DeleteDatum($input: DeleteDatumInput!) { @@ -988,9 +972,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("viewer cannot delete", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Delete Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Delete Test").Create() _, err := viewer.Do(` mutation DeleteDatum($input: DeleteDatumInput!) { @@ -1008,9 +991,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("read", func(t *testing.T) { t.Run("owner can read", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Read Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Read Test").Create() var result struct { Node *struct { @@ -1033,9 +1015,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("admin can read", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Read Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Read Test").Create() var result struct { Node *struct { @@ -1058,9 +1039,8 @@ func TestDatum_RBAC(t *testing.T) { t.Run("viewer can read", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - datumID := factory.NewDatum(owner, profileID.String()).WithName("RBAC Read Test").Create() + profileID := factory.CreateUser(owner) + datumID := factory.NewDatum(owner, profileID).WithName("RBAC Read Test").Create() var result struct { Node *struct { @@ -1085,8 +1065,7 @@ func TestDatum_RBAC(t *testing.T) { func TestDatum_MaxLength_Validation(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) longName := strings.Repeat("a", 1001) @@ -1104,7 +1083,7 @@ func TestDatum_MaxLength_Validation(t *testing.T) { _, err := owner.Do(query, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "ownerId": profileID.String(), + "ownerId": profileID, "name": longName, "dataClassification": "INTERNAL", }, @@ -1114,7 +1093,7 @@ func TestDatum_MaxLength_Validation(t *testing.T) { }) t.Run("update", func(t *testing.T) { - datumID := factory.NewDatum(owner, profileID.String()).WithName("Max Length Test").Create() + datumID := factory.NewDatum(owner, profileID).WithName("Max Length Test").Create() query := ` mutation UpdateDatum($input: UpdateDatumInput!) { @@ -1138,11 +1117,10 @@ func TestDatum_MaxLength_Validation(t *testing.T) { func TestDatum_Pagination(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) for i := 0; i < 5; i++ { - factory.NewDatum(owner, profileID.String()). + factory.NewDatum(owner, profileID). WithName(fmt.Sprintf("Pagination Datum %d", i)). Create() } @@ -1288,9 +1266,8 @@ func TestDatum_TenantIsolation(t *testing.T) { org1Owner := testutil.NewClient(t, testutil.RoleOwner) org2Owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, org1Owner).GetProfileID() - datumID := factory.NewDatum(org1Owner, profileID.String()).WithName("Org1 Datum").Create() + profileID := factory.CreateUser(org1Owner) + datumID := factory.NewDatum(org1Owner, profileID).WithName("Org1 Datum").Create() t.Run("cannot read datum from another organization", func(t *testing.T) { query := ` @@ -1396,11 +1373,10 @@ func TestDatum_TenantIsolation(t *testing.T) { func TestDatum_Ordering(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // TODO: right now we need to invite and accept invite to get new profile. - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) - factory.NewDatum(owner, profileID.String()).WithName("AAA Order Test").Create() - factory.NewDatum(owner, profileID.String()).WithName("ZZZ Order Test").Create() + factory.NewDatum(owner, profileID).WithName("AAA Order Test").Create() + factory.NewDatum(owner, profileID).WithName("ZZZ Order Test").Create() t.Run("order by created_at descending", func(t *testing.T) { query := ` diff --git a/e2e/console/document_test.go b/e2e/console/document_test.go index b3122d84d..a8100f25b 100644 --- a/e2e/console/document_test.go +++ b/e2e/console/document_test.go @@ -29,7 +29,7 @@ import ( func TestDocument_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) tests := []struct { name string @@ -113,7 +113,7 @@ func TestDocument_Create(t *testing.T) { input := map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "approverIds": []string{approverProfileID.String()}, + "approverIds": []string{approverProfileID}, } for k, v := range tt.input { input[k] = v @@ -151,7 +151,7 @@ func TestDocument_Create(t *testing.T) { func TestDocument_Create_Validation(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) tests := []struct { name string @@ -293,7 +293,7 @@ func TestDocument_Create_Validation(t *testing.T) { input["organizationId"] = owner.GetOrganizationID().String() } if !tt.skipApprover { - input["approverIds"] = []string{approverProfileID.String()} + input["approverIds"] = []string{approverProfileID} } for k, v := range tt.input { input[k] = v @@ -309,7 +309,7 @@ func TestDocument_Create_Validation(t *testing.T) { func TestDocument_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) tests := []struct { name string @@ -321,7 +321,7 @@ func TestDocument_Update(t *testing.T) { { name: "update title", setup: func() string { - return factory.NewDocument(owner, approverProfileID.String()). + return factory.NewDocument(owner, approverProfileID). WithTitle("Document to Update"). Create() }, @@ -337,7 +337,7 @@ func TestDocument_Update(t *testing.T) { { name: "update document type", setup: func() string { - return factory.NewDocument(owner, approverProfileID.String()). + return factory.NewDocument(owner, approverProfileID). WithTitle("Type Test"). WithDocumentType("POLICY"). Create() @@ -393,8 +393,8 @@ func TestDocument_Update(t *testing.T) { func TestDocument_Update_Validation(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - baseDocumentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("Validation Test Document").Create() + approverProfileID := factory.CreateUser(owner) + baseDocumentID := factory.NewDocument(owner, approverProfileID).WithTitle("Validation Test Document").Create() tests := []struct { name string @@ -484,10 +484,10 @@ func TestDocument_Update_Validation(t *testing.T) { func TestDocument_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) t.Run("delete existing document", func(t *testing.T) { - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("Document to Delete").Create() + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("Document to Delete").Create() query := ` mutation DeleteDocument($input: DeleteDocumentInput!) { @@ -549,11 +549,11 @@ func TestDocument_Delete_Validation(t *testing.T) { func TestDocument_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) documentTitles := []string{"Document A", "Document B", "Document C"} for _, title := range documentTitles { - factory.NewDocument(owner, approverProfileID.String()).WithTitle(title).Create() + factory.NewDocument(owner, approverProfileID).WithTitle(title).Create() } query := ` @@ -621,7 +621,7 @@ func TestDocument_Query(t *testing.T) { func TestDocument_Timestamps(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) t.Run("createdAt and updatedAt are set on create", func(t *testing.T) { beforeCreate := time.Now().Add(-time.Second) @@ -655,7 +655,7 @@ func TestDocument_Timestamps(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "approverIds": []string{approverProfileID.String()}, + "approverIds": []string{approverProfileID}, "title": "Timestamp Test Document", "content": "Test content", "documentType": "POLICY", @@ -669,7 +669,7 @@ func TestDocument_Timestamps(t *testing.T) { }) t.Run("updatedAt changes on update", func(t *testing.T) { - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("Timestamp Update Test").Create() + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("Timestamp Update Test").Create() getQuery := ` query($id: ID!) { @@ -733,8 +733,8 @@ func TestDocument_Timestamps(t *testing.T) { func TestDocument_SubResolvers(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("SubResolver Test Document").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("SubResolver Test Document").Create() t.Run("approvers sub-resolver", func(t *testing.T) { query := ` @@ -775,7 +775,7 @@ func TestDocument_SubResolvers(t *testing.T) { require.NoError(t, err) assert.Equal(t, 1, result.Node.Approvers.TotalCount) require.Len(t, result.Node.Approvers.Edges, 1) - assert.Equal(t, approverProfileID.String(), result.Node.Approvers.Edges[0].Node.ID) + assert.Equal(t, approverProfileID, result.Node.Approvers.Edges[0].Node.ID) }) t.Run("organization sub-resolver", func(t *testing.T) { @@ -816,7 +816,7 @@ func TestDocument_RBAC(t *testing.T) { t.Run("create", func(t *testing.T) { t.Run("owner can create", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) _, err := owner.Do(` mutation CreateDocument($input: CreateDocumentInput!) { @@ -827,7 +827,7 @@ func TestDocument_RBAC(t *testing.T) { `, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "approverIds": []string{approverProfileID.String()}, + "approverIds": []string{approverProfileID}, "title": "RBAC Test Document", "content": "Test content", "documentType": "POLICY", @@ -840,7 +840,7 @@ func TestDocument_RBAC(t *testing.T) { t.Run("admin can create", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) _, err := admin.Do(` mutation CreateDocument($input: CreateDocumentInput!) { @@ -851,7 +851,7 @@ func TestDocument_RBAC(t *testing.T) { `, map[string]any{ "input": map[string]any{ "organizationId": admin.GetOrganizationID().String(), - "approverIds": []string{approverProfileID.String()}, + "approverIds": []string{approverProfileID}, "title": "RBAC Test Document", "content": "Test content", "documentType": "POLICY", @@ -864,7 +864,7 @@ func TestDocument_RBAC(t *testing.T) { t.Run("viewer cannot create", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) _, err := viewer.Do(` mutation CreateDocument($input: CreateDocumentInput!) { @@ -875,7 +875,7 @@ func TestDocument_RBAC(t *testing.T) { `, map[string]any{ "input": map[string]any{ "organizationId": viewer.GetOrganizationID().String(), - "approverIds": []string{approverProfileID.String()}, + "approverIds": []string{approverProfileID}, "title": "RBAC Test Document", "content": "Test content", "documentType": "POLICY", @@ -889,8 +889,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("update", func(t *testing.T) { t.Run("owner can update", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Update Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Update Test").Create() _, err := owner.Do(` mutation UpdateDocument($input: UpdateDocumentInput!) { @@ -910,8 +910,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("admin can update", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Update Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Update Test").Create() _, err := admin.Do(` mutation UpdateDocument($input: UpdateDocumentInput!) { @@ -931,8 +931,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("viewer cannot update", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Update Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Update Test").Create() _, err := viewer.Do(` mutation UpdateDocument($input: UpdateDocumentInput!) { @@ -953,8 +953,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("delete", func(t *testing.T) { t.Run("owner can delete", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Delete Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Delete Test").Create() _, err := owner.Do(` mutation DeleteDocument($input: DeleteDocumentInput!) { @@ -971,8 +971,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("admin can delete", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Delete Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Delete Test").Create() _, err := admin.Do(` mutation DeleteDocument($input: DeleteDocumentInput!) { @@ -989,8 +989,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("viewer cannot delete", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Delete Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Delete Test").Create() _, err := viewer.Do(` mutation DeleteDocument($input: DeleteDocumentInput!) { @@ -1008,8 +1008,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("read", func(t *testing.T) { t.Run("owner can read", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Read Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Read Test").Create() var result struct { Node *struct { @@ -1032,8 +1032,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("admin can read", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Read Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Read Test").Create() var result struct { Node *struct { @@ -1056,8 +1056,8 @@ func TestDocument_RBAC(t *testing.T) { t.Run("viewer can read", func(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("RBAC Read Test").Create() + approverProfileID := factory.CreateUser(owner) + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("RBAC Read Test").Create() var result struct { Node *struct { @@ -1082,7 +1082,7 @@ func TestDocument_RBAC(t *testing.T) { func TestDocument_MaxLength_Validation(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) longTitle := strings.Repeat("a", 1001) @@ -1100,7 +1100,7 @@ func TestDocument_MaxLength_Validation(t *testing.T) { _, err := owner.Do(query, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "approverIds": []string{approverProfileID.String()}, + "approverIds": []string{approverProfileID}, "title": longTitle, "content": "Test content", "documentType": "POLICY", @@ -1112,7 +1112,7 @@ func TestDocument_MaxLength_Validation(t *testing.T) { }) t.Run("update", func(t *testing.T) { - documentID := factory.NewDocument(owner, approverProfileID.String()).WithTitle("Max Length Test").Create() + documentID := factory.NewDocument(owner, approverProfileID).WithTitle("Max Length Test").Create() query := ` mutation UpdateDocument($input: UpdateDocumentInput!) { @@ -1136,10 +1136,10 @@ func TestDocument_MaxLength_Validation(t *testing.T) { func TestDocument_Pagination(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) for i := 0; i < 5; i++ { - factory.NewDocument(owner, approverProfileID.String()). + factory.NewDocument(owner, approverProfileID). WithTitle(fmt.Sprintf("Pagination Document %d", i)). Create() } @@ -1285,8 +1285,8 @@ func TestDocument_TenantIsolation(t *testing.T) { org1Owner := testutil.NewClient(t, testutil.RoleOwner) org2Owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, org1Owner).GetProfileID() - documentID := factory.NewDocument(org1Owner, approverProfileID.String()).WithTitle("Org1 Document").Create() + approverProfileID := factory.CreateUser(org1Owner) + documentID := factory.NewDocument(org1Owner, approverProfileID).WithTitle("Org1 Document").Create() t.Run("cannot read document from another organization", func(t *testing.T) { query := ` @@ -1392,10 +1392,10 @@ func TestDocument_TenantIsolation(t *testing.T) { func TestDocument_Ordering(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - approverProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + approverProfileID := factory.CreateUser(owner) - factory.NewDocument(owner, approverProfileID.String()).WithTitle("AAA Order Test").Create() - factory.NewDocument(owner, approverProfileID.String()).WithTitle("ZZZ Order Test").Create() + factory.NewDocument(owner, approverProfileID).WithTitle("AAA Order Test").Create() + factory.NewDocument(owner, approverProfileID).WithTitle("ZZZ Order Test").Create() t.Run("order by created_at descending", func(t *testing.T) { query := ` diff --git a/e2e/console/document_version_test.go b/e2e/console/document_version_test.go index 96281510b..9512fd60e 100644 --- a/e2e/console/document_version_test.go +++ b/e2e/console/document_version_test.go @@ -19,13 +19,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) // createTestDocument creates a document and returns its ID and the document version ID func createTestDocument(t *testing.T, owner *testutil.Client) (docID string, docVersionID string) { t.Helper() - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) query := ` mutation CreateDocument($input: CreateDocumentInput!) { @@ -68,7 +69,7 @@ func createTestDocument(t *testing.T, owner *testutil.Client) (docID string, doc "organizationId": owner.GetOrganizationID().String(), "title": "Test Document", "content": "Initial content", - "approverIds": []string{profileID.String()}, + "approverIds": []string{profileID}, "documentType": "POLICY", "classification": "INTERNAL", }, @@ -263,7 +264,7 @@ func TestDocumentVersion_RequestSignature(t *testing.T) { publishedVersionID := publishResult.PublishDocumentVersion.DocumentVersion.ID // Create a person to sign - signerProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + signerProfileID := factory.CreateUser(owner) query := ` mutation RequestSignature($input: RequestSignatureInput!) { @@ -300,14 +301,14 @@ func TestDocumentVersion_RequestSignature(t *testing.T) { err = owner.Execute(query, map[string]any{ "input": map[string]any{ "documentVersionId": publishedVersionID, - "signatoryId": signerProfileID.String(), + "signatoryId": signerProfileID, }, }, &result) require.NoError(t, err) assert.NotEmpty(t, result.RequestSignature.DocumentVersionSignatureEdge.Node.ID) assert.Equal(t, "REQUESTED", result.RequestSignature.DocumentVersionSignatureEdge.Node.State) - assert.Equal(t, signerProfileID.String(), result.RequestSignature.DocumentVersionSignatureEdge.Node.SignedBy.ID) + assert.Equal(t, signerProfileID, result.RequestSignature.DocumentVersionSignatureEdge.Node.SignedBy.ID) } func TestDocumentVersion_BulkPublish(t *testing.T) { @@ -382,8 +383,8 @@ func TestDocumentVersion_BulkRequestSignatures(t *testing.T) { require.NoError(t, err) // Create multiple signers - signer1ProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - signer2ProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + signer1ProfileID := factory.CreateUser(owner) + signer2ProfileID := factory.CreateUser(owner) query := ` mutation BulkRequestSignatures($input: BulkRequestSignaturesInput!) { @@ -412,7 +413,7 @@ func TestDocumentVersion_BulkRequestSignatures(t *testing.T) { err = owner.Execute(query, map[string]any{ "input": map[string]any{ "documentIds": []string{docID}, - "signatoryIds": []string{signer1ProfileID.String(), signer2ProfileID.String()}, + "signatoryIds": []string{signer1ProfileID, signer2ProfileID}, }, }, &result) require.NoError(t, err) diff --git a/e2e/console/mapping_test.go b/e2e/console/mapping_test.go index ce71c3a3e..db0c25969 100644 --- a/e2e/console/mapping_test.go +++ b/e2e/console/mapping_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) @@ -367,7 +368,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { controlID := createControlResult.CreateControl.ControlEdge.Node.ID // Create a document - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) var createDocumentResult struct { CreateDocument struct { DocumentEdge struct { @@ -392,7 +393,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "title": "Document for Control Mapping", "content": "Document content", - "approverIds": []string{profileID.String()}, + "approverIds": []string{profileID}, "documentType": "POLICY", "classification": "INTERNAL", }, @@ -755,7 +756,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) { riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID // Create a document - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) var createDocumentResult struct { CreateDocument struct { DocumentEdge struct { @@ -780,7 +781,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "title": "Document for Risk Mapping", "content": "Document content", - "approverIds": []string{profileID.String()}, + "approverIds": []string{profileID}, "documentType": "POLICY", "classification": "INTERNAL", }, @@ -869,7 +870,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) { riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID // Create an obligation - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) var createObligationResult struct { CreateObligation struct { ObligationEdge struct { @@ -894,7 +895,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "area": "Risk Management", "requirement": "Obligation for Risk Mapping", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "NON_COMPLIANT", "type": "LEGAL", }, diff --git a/e2e/console/meeting_test.go b/e2e/console/meeting_test.go index 913489a58..29461dbd2 100644 --- a/e2e/console/meeting_test.go +++ b/e2e/console/meeting_test.go @@ -690,8 +690,8 @@ func TestMeeting_SubResolvers_WithData(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - attendee1ProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() - attendee2ProfileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + attendee1ProfileID := factory.CreateUser(owner) + attendee2ProfileID := factory.CreateUser(owner) var result struct { CreateMeeting struct { @@ -730,7 +730,7 @@ func TestMeeting_SubResolvers_WithData(t *testing.T) { "organizationId": owner.GetOrganizationID().String(), "name": "Meeting With Attendees", "date": time.Now().Format(time.RFC3339Nano), - "attendeeIds": []string{attendee1ProfileID.String(), attendee2ProfileID.String()}, + "attendeeIds": []string{attendee1ProfileID, attendee2ProfileID}, }, }, &result) require.NoError(t, err) diff --git a/e2e/console/nonconformity_test.go b/e2e/console/nonconformity_test.go index e933d00f9..52d685b61 100644 --- a/e2e/console/nonconformity_test.go +++ b/e2e/console/nonconformity_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) @@ -99,7 +100,7 @@ func createAuditForNC(t *testing.T, owner *testutil.Client, name string) string func TestNonconformity_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) auditID := createAuditForNC(t, owner, "NC Test Audit") query := ` @@ -142,7 +143,7 @@ func TestNonconformity_Create(t *testing.T) { "auditId": auditID, "rootCause": "Insufficient access controls", "correctiveAction": "Implement MFA", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", }, }, &result) @@ -158,7 +159,7 @@ func TestNonconformity_Create(t *testing.T) { func TestNonconformity_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) auditID := createAuditForNC(t, owner, "NC Update Test Audit") // Create a nonconformity to update @@ -190,7 +191,7 @@ func TestNonconformity_Update(t *testing.T) { "referenceId": fmt.Sprintf("NC-UPDATE-%d", time.Now().UnixNano()), "auditId": auditID, "rootCause": "Original root cause", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", }, }, &createResult) @@ -241,7 +242,7 @@ func TestNonconformity_Update(t *testing.T) { func TestNonconformity_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) auditID := createAuditForNC(t, owner, "NC Delete Test Audit") // Create a nonconformity to delete @@ -273,7 +274,7 @@ func TestNonconformity_Delete(t *testing.T) { "referenceId": fmt.Sprintf("NC-DELETE-%d", time.Now().UnixNano()), "auditId": auditID, "rootCause": "Test root cause", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", }, }, &createResult) @@ -306,7 +307,7 @@ func TestNonconformity_Delete(t *testing.T) { func TestNonconformity_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) auditID := createAuditForNC(t, owner, "NC List Test Audit") // Create multiple nonconformities @@ -339,7 +340,7 @@ func TestNonconformity_List(t *testing.T) { "referenceId": fmt.Sprintf("NC-LIST-%d-%d", i, time.Now().UnixNano()), "auditId": auditID, "rootCause": fmt.Sprintf("Root cause %d", i), - "ownerId": profileID.String(), + "ownerId": profileID, "status": "OPEN", }, }, &createResult) @@ -390,7 +391,7 @@ func TestNonconformity_List(t *testing.T) { func TestNonconformity_StatusValues(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) auditID := createAuditForNC(t, owner, "NC Status Test Audit") statuses := []string{"OPEN", "IN_PROGRESS", "CLOSED"} @@ -427,7 +428,7 @@ func TestNonconformity_StatusValues(t *testing.T) { "referenceId": fmt.Sprintf("NC-STATUS-%s-%d", status, time.Now().UnixNano()), "auditId": auditID, "rootCause": "Test root cause", - "ownerId": profileID.String(), + "ownerId": profileID, "status": status, }, }, &result) diff --git a/e2e/console/obligation_test.go b/e2e/console/obligation_test.go index 062b90050..3052a3e88 100644 --- a/e2e/console/obligation_test.go +++ b/e2e/console/obligation_test.go @@ -19,13 +19,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) func TestObligation_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) query := ` mutation CreateObligation($input: CreateObligationInput!) { @@ -66,7 +67,7 @@ func TestObligation_Create(t *testing.T) { "source": "GDPR Article 5", "requirement": "Data must be processed lawfully", "regulator": "ICO", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "NON_COMPLIANT", "type": "LEGAL", }, @@ -84,7 +85,7 @@ func TestObligation_Create(t *testing.T) { func TestObligation_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) // Create an obligation to update createQuery := ` @@ -113,7 +114,7 @@ func TestObligation_Update(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "area": "Original Area", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "NON_COMPLIANT", "type": "LEGAL", }, @@ -161,7 +162,7 @@ func TestObligation_Update(t *testing.T) { func TestObligation_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) // Create an obligation to delete createQuery := ` @@ -190,7 +191,7 @@ func TestObligation_Delete(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "area": "Obligation to Delete", - "ownerId": profileID.String(), + "ownerId": profileID, "status": "NON_COMPLIANT", "type": "LEGAL", }, @@ -225,7 +226,7 @@ func TestObligation_Delete(t *testing.T) { func TestObligation_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) // Create multiple obligations areas := []string{"Area A", "Area B", "Area C"} @@ -256,7 +257,7 @@ func TestObligation_List(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "area": area, - "ownerId": profileID.String(), + "ownerId": profileID, "status": "NON_COMPLIANT", "type": "LEGAL", }, @@ -309,7 +310,7 @@ func TestObligation_List(t *testing.T) { func TestObligation_StatusValues(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) statuses := []string{"NON_COMPLIANT", "PARTIALLY_COMPLIANT", "COMPLIANT"} @@ -343,7 +344,7 @@ func TestObligation_StatusValues(t *testing.T) { "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "area": "Status Test " + status, - "ownerId": profileID.String(), + "ownerId": profileID, "status": status, "type": "LEGAL", }, diff --git a/e2e/console/rbac_test.go b/e2e/console/rbac_test.go index 88131aa77..b1fdbf8fc 100644 --- a/e2e/console/rbac_test.go +++ b/e2e/console/rbac_test.go @@ -221,11 +221,11 @@ const ( } }` - listMembersQuery = ` - query GetMembers($id: ID!) { + listUsersQuery = ` + query GetProfiles($id: ID!) { node(id: $id) { ... on Organization { - members(first: 10) { totalCount } + profiles(first: 10) { totalCount } } } }` @@ -1063,10 +1063,10 @@ func TestRBAC(t *testing.T) { useConnect: true, }, { - name: "owner can list members", + name: "owner can list users", role: "owner", client: owner, - query: listMembersQuery, + query: listUsersQuery, variables: func() map[string]any { return map[string]any{"id": owner.GetOrganizationID().String()} }, @@ -1074,10 +1074,10 @@ func TestRBAC(t *testing.T) { useConnect: true, }, { - name: "admin can list members", + name: "admin can list users", role: "admin", client: admin, - query: listMembersQuery, + query: listUsersQuery, variables: func() map[string]any { return map[string]any{"id": owner.GetOrganizationID().String()} }, @@ -1085,10 +1085,10 @@ func TestRBAC(t *testing.T) { useConnect: true, }, { - name: "viewer can list members", + name: "viewer can list users", role: "viewer", client: viewer, - query: listMembersQuery, + query: listUsersQuery, variables: func() map[string]any { return map[string]any{"id": owner.GetOrganizationID().String()} }, diff --git a/e2e/console/risk_test.go b/e2e/console/risk_test.go index 84745a84a..870083cac 100644 --- a/e2e/console/risk_test.go +++ b/e2e/console/risk_test.go @@ -881,7 +881,7 @@ func TestRisk_OmittableOwner(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) // Create a people for owner assignment - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) riskID := factory.NewRisk(owner).WithName("Owner Test Risk").Create() t.Run("set owner", func(t *testing.T) { @@ -914,11 +914,11 @@ func TestRisk_OmittableOwner(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "id": riskID, - "ownerId": profileID.String(), + "ownerId": profileID, }, }, &result) require.NoError(t, err) - assert.Equal(t, profileID.String(), result.UpdateRisk.Risk.Owner.ID) + assert.Equal(t, profileID, result.UpdateRisk.Risk.Owner.ID) }) t.Run("clear owner with null", func(t *testing.T) { diff --git a/e2e/console/task_assignment_test.go b/e2e/console/task_assignment_test.go index 9566fdad0..c5a77a4be 100644 --- a/e2e/console/task_assignment_test.go +++ b/e2e/console/task_assignment_test.go @@ -30,7 +30,7 @@ func TestTask_Assign(t *testing.T) { // Create measure and task measureID := factory.NewMeasure(owner).Create() taskID := factory.NewTask(owner, measureID).Create() - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) query := ` mutation UpdateTask($input: UpdateTaskInput!) { @@ -61,13 +61,13 @@ func TestTask_Assign(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "taskId": taskID, - "assignedToId": profileID.String(), + "assignedToId": profileID, }, }, &result) require.NoError(t, err) assert.Equal(t, taskID, result.UpdateTask.Task.ID) - assert.Equal(t, profileID.String(), result.UpdateTask.Task.AssignedTo.ID) + assert.Equal(t, profileID, result.UpdateTask.Task.AssignedTo.ID) } func TestTask_Unassign(t *testing.T) { @@ -77,7 +77,7 @@ func TestTask_Unassign(t *testing.T) { // Create measure, task, people and assign measureID := factory.NewMeasure(owner).Create() taskID := factory.NewTask(owner, measureID).Create() - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) // First assign the task assignQuery := ` @@ -93,7 +93,7 @@ func TestTask_Unassign(t *testing.T) { _, err := owner.Do(assignQuery, map[string]any{ "input": map[string]any{ "taskId": taskID, - "assignedToId": profileID.String(), + "assignedToId": profileID, }, }) require.NoError(t, err) diff --git a/e2e/console/task_test.go b/e2e/console/task_test.go index 467cd191b..dcfc03406 100644 --- a/e2e/console/task_test.go +++ b/e2e/console/task_test.go @@ -650,7 +650,7 @@ func TestTask_OmittableAssignee(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) // Create a profile for assignee - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) measureID := factory.NewMeasure(owner). WithName("Task Assignee Test"). Create() @@ -689,11 +689,11 @@ func TestTask_OmittableAssignee(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "taskId": taskID, - "assignedToId": profileID.String(), + "assignedToId": profileID, }, }, &result) require.NoError(t, err) - assert.Equal(t, profileID.String(), result.UpdateTask.Task.AssignedTo.ID) + assert.Equal(t, profileID, result.UpdateTask.Task.AssignedTo.ID) }) t.Run("clear assignee", func(t *testing.T) { diff --git a/e2e/console/testdata/config.yaml b/e2e/console/testdata/config.yaml index a92289933..f3b6daeea 100644 --- a/e2e/console/testdata/config.yaml +++ b/e2e/console/testdata/config.yaml @@ -57,7 +57,7 @@ probod: smtp: addr: "localhost:1025" tls-required: false - mailer-interval: 60 + mailer-interval: 1 slack: sender-interval: 60 diff --git a/e2e/console/member_test.go b/e2e/console/user_test.go similarity index 52% rename from e2e/console/member_test.go rename to e2e/console/user_test.go index ce5b4b5b0..5fa150eae 100644 --- a/e2e/console/member_test.go +++ b/e2e/console/user_test.go @@ -15,32 +15,32 @@ package console_test import ( - "fmt" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.probo.inc/probo/e2e/internal/testutil" ) -func TestMember_UpdateMembership(t *testing.T) { +func TestUser_UpdateMembership(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) // Create an admin to update _ = testutil.NewClientInOrg(t, testutil.RoleAdmin, owner) - // Get the member ID of the admin + // Get the user ID of the admin query := ` query($id: ID!) { node(id: $id) { ... on Organization { - members(first: 10) { + profiles(first: 10) { edges { node { - id - role + membership { + id + role + } } } } @@ -51,14 +51,16 @@ func TestMember_UpdateMembership(t *testing.T) { var result struct { Node struct { - Members struct { + Profiles struct { Edges []struct { Node struct { - ID string `json:"id"` - Role string `json:"role"` + Membership struct { + ID string `json:"id"` + Role string `json:"role"` + } `json:"membership"` } `json:"node"` } `json:"edges"` - } `json:"members"` + } `json:"profiles"` } `json:"node"` } @@ -67,15 +69,15 @@ func TestMember_UpdateMembership(t *testing.T) { }, &result) require.NoError(t, err) - // Find the admin member - var adminMemberID string - for _, edge := range result.Node.Members.Edges { - if edge.Node.Role == "ADMIN" { - adminMemberID = edge.Node.ID + // Find the admin + var adminMembershipID string + for _, edge := range result.Node.Profiles.Edges { + if edge.Node.Membership.Role == "ADMIN" { + adminMembershipID = edge.Node.Membership.ID break } } - require.NotEmpty(t, adminMemberID, "Should find admin member") + require.NotEmpty(t, adminMembershipID, "Should find admin member") // Update the member role to VIEWER mutation := ` @@ -101,7 +103,7 @@ func TestMember_UpdateMembership(t *testing.T) { err = owner.ExecuteConnect(mutation, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "membershipId": adminMemberID, + "membershipId": adminMembershipID, "role": "VIEWER", }, }, &mutationResult) @@ -110,24 +112,26 @@ func TestMember_UpdateMembership(t *testing.T) { assert.Equal(t, "VIEWER", mutationResult.UpdateMembership.Membership.Role) } -func TestMember_RemoveMember(t *testing.T) { +func TestUser_RemoveUser(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) - // Create a member to remove - memberToRemove := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) - _ = memberToRemove + // Create a user to remove + userToRemove := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + _ = userToRemove - // Get the member ID + // Get the user ID query := ` query($id: ID!) { node(id: $id) { ... on Organization { - members(first: 50) { + profiles(first: 50) { edges { node { id - role + membership { + role + } } } } @@ -138,14 +142,16 @@ func TestMember_RemoveMember(t *testing.T) { var result struct { Node struct { - Members struct { + Profiles struct { Edges []struct { Node struct { - ID string `json:"id"` - Role string `json:"role"` + ID string `json:"id"` + Membership struct { + Role string `json:"role"` + } `json:"membership"` } `json:"node"` } `json:"edges"` - } `json:"members"` + } `json:"profiles"` } `json:"node"` } @@ -154,113 +160,43 @@ func TestMember_RemoveMember(t *testing.T) { }, &result) require.NoError(t, err) - // Find a viewer member to remove - var memberID string - for _, edge := range result.Node.Members.Edges { - if edge.Node.Role == "VIEWER" { - memberID = edge.Node.ID + // Find a viewer user to remove + var userID string + for _, edge := range result.Node.Profiles.Edges { + if edge.Node.Membership.Role == "VIEWER" { + userID = edge.Node.ID break } } - assert.NotEmpty(t, memberID, "Should find viewer member") + assert.NotEmpty(t, userID, "Should find viewer member") // Remove the member mutation := ` - mutation($input: RemoveMemberInput!) { - removeMember(input: $input) { - deletedMembershipId + mutation($input: RemoveUserInput!) { + removeUser(input: $input) { + deletedProfileId } } ` var mutationResult struct { - RemoveMember struct { - DeletedMembershipID string `json:"deletedMembershipId"` - } `json:"removeMember"` + RemoveUser struct { + DeletedProfileID string `json:"deletedProfileId"` + } `json:"removeUser"` } err = owner.ExecuteConnect(mutation, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), - "membershipId": memberID, + "profileId": userID, }, }, &mutationResult) require.NoError(t, err) - assert.Equal(t, memberID, mutationResult.RemoveMember.DeletedMembershipID) + assert.Equal(t, userID, mutationResult.RemoveUser.DeletedProfileID) } -func TestInvitation_Delete(t *testing.T) { - t.Parallel() - owner := testutil.NewClient(t, testutil.RoleOwner) - - // Create an invitation - inviteMutation := ` - mutation($input: InviteMemberInput!) { - inviteMember(input: $input) { - invitationEdge { - node { - id - email - status - } - } - } - } - ` - - var inviteResult struct { - InviteMember struct { - InvitationEdge struct { - Node struct { - ID string `json:"id"` - Email string `json:"email"` - Status string `json:"status"` - } `json:"node"` - } `json:"invitationEdge"` - } `json:"inviteMember"` - } - - err := owner.ExecuteConnect(inviteMutation, map[string]any{ - "input": map[string]any{ - "organizationId": owner.GetOrganizationID().String(), - "email": fmt.Sprintf("invite.delete.%d@example.com", time.Now().UnixNano()), - "fullName": "Test User", - "role": "VIEWER", - }, - }, &inviteResult) - require.NoError(t, err) - - invitationID := inviteResult.InviteMember.InvitationEdge.Node.ID - assert.NotEmpty(t, invitationID) - - // Delete the invitation - deleteMutation := ` - mutation($input: DeleteInvitationInput!) { - deleteInvitation(input: $input) { - deletedInvitationId - } - } - ` - - var deleteResult struct { - DeleteInvitation struct { - DeletedInvitationID string `json:"deletedInvitationId"` - } `json:"deleteInvitation"` - } - - err = owner.ExecuteConnect(deleteMutation, map[string]any{ - "input": map[string]any{ - "organizationId": owner.GetOrganizationID().String(), - "invitationId": invitationID, - }, - }, &deleteResult) - require.NoError(t, err) - - assert.Equal(t, invitationID, deleteResult.DeleteInvitation.DeletedInvitationID) -} - -func TestMember_List(t *testing.T) { +func TestUser_List(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) @@ -272,11 +208,13 @@ func TestMember_List(t *testing.T) { query($id: ID!) { node(id: $id) { ... on Organization { - members(first: 10) { + profiles(first: 10) { edges { node { id - role + membership { + role + } } } totalCount @@ -288,15 +226,17 @@ func TestMember_List(t *testing.T) { var result struct { Node struct { - Members struct { + Profiles struct { Edges []struct { Node struct { - ID string `json:"id"` - Role string `json:"role"` + ID string `json:"id"` + Membership struct { + Role string `json:"role"` + } `json:"membership"` } `json:"node"` } `json:"edges"` TotalCount int `json:"totalCount"` - } `json:"members"` + } `json:"profiles"` } `json:"node"` } @@ -305,5 +245,5 @@ func TestMember_List(t *testing.T) { }, &result) require.NoError(t, err) - assert.GreaterOrEqual(t, result.Node.Members.TotalCount, 3, "Should have at least 3 members") + assert.GreaterOrEqual(t, result.Node.Profiles.TotalCount, 3, "Should have at least 3 members") } diff --git a/e2e/console/vendor_test.go b/e2e/console/vendor_test.go index ded3f2891..a0cdda89c 100644 --- a/e2e/console/vendor_test.go +++ b/e2e/console/vendor_test.go @@ -755,7 +755,7 @@ func TestVendor_OmittableBusinessOwner(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) // Create a profile for owner assignment - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) vendorID := factory.NewVendor(owner). WithName("BusinessOwner Test Vendor"). Create() @@ -790,11 +790,11 @@ func TestVendor_OmittableBusinessOwner(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "id": vendorID, - "businessOwnerId": profileID.String(), + "businessOwnerId": profileID, }, }, &result) require.NoError(t, err) - assert.Equal(t, profileID.String(), result.UpdateVendor.Vendor.BusinessOwner.ID) + assert.Equal(t, profileID, result.UpdateVendor.Vendor.BusinessOwner.ID) }) t.Run("clear business owner with null", func(t *testing.T) { @@ -838,7 +838,7 @@ func TestVendor_OmittableSecurityOwner(t *testing.T) { owner := testutil.NewClient(t, testutil.RoleOwner) // Create a profile for owner assignment - profileID := testutil.NewClientInOrg(t, testutil.RoleViewer, owner).GetProfileID() + profileID := factory.CreateUser(owner) vendorID := factory.NewVendor(owner).WithName("SecurityOwner Test Vendor").Create() t.Run("set security owner", func(t *testing.T) { @@ -871,11 +871,11 @@ func TestVendor_OmittableSecurityOwner(t *testing.T) { err := owner.Execute(query, map[string]any{ "input": map[string]any{ "id": vendorID, - "securityOwnerId": profileID.String(), + "securityOwnerId": profileID, }, }, &result) require.NoError(t, err) - assert.Equal(t, profileID.String(), result.UpdateVendor.Vendor.SecurityOwner.ID) + assert.Equal(t, profileID, result.UpdateVendor.Vendor.SecurityOwner.ID) }) t.Run("clear security owner with null", func(t *testing.T) { diff --git a/e2e/internal/factory/factory.go b/e2e/internal/factory/factory.go index dcc6e3b00..ae6e1b8ec 100644 --- a/e2e/internal/factory/factory.go +++ b/e2e/internal/factory/factory.go @@ -22,6 +22,7 @@ import ( "github.com/brianvoe/gofakeit/v7" "github.com/stretchr/testify/require" "go.probo.inc/probo/e2e/internal/testutil" + "go.probo.inc/probo/pkg/coredata" ) func SafeName(prefix string) string { @@ -92,6 +93,52 @@ func (a Attrs) getBool(key string, defaultVal bool) bool { return defaultVal } +func CreateUser(c *testutil.Client, attrs ...Attrs) string { + c.T.Helper() + + var a Attrs + if len(attrs) > 0 { + a = attrs[0] + } + + const query = ` + mutation($input: CreateUserInput!) { + createUser(input: $input) { + profileEdge { + node { id } + } + } + } + ` + + input := map[string]any{ + "organizationId": c.GetOrganizationID(), + "emailAddress": a.getString("emailAddress", SafeEmail()), + "fullName": a.getString("fullName", SafeName("User")), + "role": a.getString("role", "EMPLOYEE"), + "kind": coredata.MembershipProfileKindEmployee.String(), + "additionalEmailAddresses": []string{}, + } + if position := a.getStringPtr("position"); position != nil { + input["position"] = *position + } + + var result struct { + CreateUser struct { + ProfileEdge struct { + Node struct { + ID string `json:"id"` + } `json:"node"` + } `json:"profileEdge"` + } `json:"createUser"` + } + + err := c.ExecuteConnect(query, map[string]any{"input": input}, &result) + require.NoError(c.T, err, "createUser mutation failed") + + return result.CreateUser.ProfileEdge.Node.ID +} + func CreateVendor(c *testutil.Client, attrs ...Attrs) string { c.T.Helper() diff --git a/e2e/internal/testutil/client.go b/e2e/internal/testutil/client.go index f5c9dd599..b99b1e89d 100644 --- a/e2e/internal/testutil/client.go +++ b/e2e/internal/testutil/client.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/http/cookiejar" + "net/url" "testing" "time" @@ -46,9 +47,9 @@ type Client struct { T testing.TB httpClient *http.Client baseURL string + mailpitBaseURL string role TestRole userID gid.GID - profileID gid.GID organizationID gid.GID } @@ -59,9 +60,10 @@ func NewClient(t testing.TB, role TestRole) *Client { require.NoError(t, err, "cannot create cookie jar") client := &Client{ - T: t, - baseURL: GetBaseURL(), - role: role, + T: t, + baseURL: GetBaseURL(), + mailpitBaseURL: GetMailpitBaseURL(), + role: role, httpClient: &http.Client{ Jar: jar, Timeout: 30 * time.Second, @@ -82,6 +84,7 @@ func NewClientInOrg(t testing.TB, role TestRole, ownerClient *Client) *Client { client := &Client{ T: t, baseURL: GetBaseURL(), + mailpitBaseURL: GetMailpitBaseURL(), role: role, organizationID: ownerClient.organizationID, httpClient: &http.Client{ @@ -106,7 +109,7 @@ func (c *Client) setupTestUser() { // Create organization (this makes the user an OWNER) orgName := fmt.Sprintf("Test Org %s", uniqueID) - c.organizationID, c.profileID = c.createOrganization(orgName) + c.organizationID = c.createOrganization(orgName) // Assume organization session to use console API c.assumeOrganizationSession() @@ -127,10 +130,12 @@ func (c *Client) SetupTestUserInOrg(ownerClient *Client) { c.userID = c.signUp(email, password, fullName) // Owner invites user to organization - invitationID := ownerClient.inviteMember(email, fullName, coredata.MembershipRole(c.role)) + profileID := ownerClient.createUser(email, fullName, coredata.MembershipRole(c.role)) + ownerClient.inviteUser(profileID) - // New user accepts invitation - c.profileID = c.acceptInvitation(invitationID) + token := c.getActivationToken(email) + + c.activateUser(token) // Assume organization session to use console API c.assumeOrganizationSession() @@ -168,16 +173,12 @@ func (c *Client) signUp(email, password, fullName string) gid.GID { return userID } -func (c *Client) createOrganization(name string) (gid.GID, gid.GID) { +func (c *Client) createOrganization(name string) gid.GID { const query = ` mutation($input: CreateOrganizationInput!) { createOrganization(input: $input) { organization { id } - membershipEdge { - node { - profile { id } - } - } + profile { id } } } ` @@ -187,14 +188,6 @@ func (c *Client) createOrganization(name string) (gid.GID, gid.GID) { Organization struct { ID string `json:"id"` } `json:"organization"` - MembershipEdge struct { - Node struct { - ID string `json:"id"` - Profile struct { - ID string `json:"id"` - } `json:"profile"` - } `json:"node"` - } `json:"membershipEdge"` } `json:"createOrganization"` } @@ -206,10 +199,7 @@ func (c *Client) createOrganization(name string) (gid.GID, gid.GID) { orgID, err := gid.ParseGID(result.CreateOrganization.Organization.ID) require.NoError(c.T, err, "cannot parse organization ID") - profileID, err := gid.ParseGID(result.CreateOrganization.MembershipEdge.Node.Profile.ID) - require.NoError(c.T, err, "cannot parse profile ID") - - return orgID, profileID + return orgID } func (c *Client) updateOwnMembershipRole(role coredata.MembershipRole) { @@ -284,10 +274,49 @@ func (c *Client) updateOwnMembershipRole(role coredata.MembershipRole) { require.NoError(c.T, err, "updateMembership mutation failed") } -func (c *Client) inviteMember(email, fullName string, role coredata.MembershipRole) gid.GID { +func (c *Client) createUser(email, fullName string, role coredata.MembershipRole) gid.GID { const query = ` - mutation($input: InviteMemberInput!) { - inviteMember(input: $input) { + mutation($input: CreateUserInput!) { + createUser(input: $input) { + profileEdge { + node { id } + } + } + } + ` + + var result struct { + CreateUser struct { + ProfileEdge struct { + Node struct { + ID string `json:"id"` + } `json:"node"` + } `json:"profileEdge"` + } `json:"createUser"` + } + + err := c.ExecuteConnect(query, map[string]any{ + "input": map[string]any{ + "organizationId": c.organizationID.String(), + "emailAddress": email, + "fullName": fullName, + "role": string(role), + "kind": coredata.MembershipProfileKindEmployee, + "additionalEmailAddresses": []string{}, + }, + }, &result) + require.NoError(c.T, err, "createUser mutation failed") + + profileID, err := gid.ParseGID(result.CreateUser.ProfileEdge.Node.ID) + require.NoError(c.T, err, "cannot parse profile ID") + + return profileID +} + +func (c *Client) inviteUser(profileID gid.GID) { + const query = ` + mutation($input: InviteUserInput!) { + inviteUser(input: $input) { invitationEdge { node { id } } @@ -296,67 +325,79 @@ func (c *Client) inviteMember(email, fullName string, role coredata.MembershipRo ` var result struct { - InviteMember struct { + InviteUser struct { InvitationEdge struct { Node struct { ID string `json:"id"` } `json:"node"` } `json:"invitationEdge"` - } `json:"inviteMember"` + } `json:"inviteUser"` } err := c.ExecuteConnect(query, map[string]any{ "input": map[string]any{ "organizationId": c.organizationID.String(), - "email": email, - "fullName": fullName, - "role": string(role), + "profileId": profileID.String(), }, }, &result) - require.NoError(c.T, err, "inviteMember mutation failed") - - invitationID, err := gid.ParseGID(result.InviteMember.InvitationEdge.Node.ID) - require.NoError(c.T, err, "cannot parse invitation ID") - - return invitationID + require.NoError(c.T, err, "inviteUser mutation failed") } -func (c *Client) acceptInvitation(invitationID gid.GID) gid.GID { +func (c *Client) getActivationToken(email string) string { + deadline := time.Now().Add(5 * time.Second) + + for time.Now().Before(deadline) { + searchMails, err := c.SearchMails(fmt.Sprintf("to:%s subject:\"Invitation to join\"", email)) + require.NoError(c.T, err, "mailpit messages search failed") + + for _, msg := range searchMails.Messages { + linksCheck, err := c.CheckMessageLinks(msg.ID) + require.NoError(c.T, err, "mailpit link check failed") + + for _, link := range linksCheck.Links { + linkURL, err := url.Parse(link.URL) + require.NoError(c.T, err, "mailpit link invalid URL") + + query := linkURL.Query() + if query.Get("token") != "" { + return query.Get("token") + } + } + } + + time.Sleep(100 * time.Millisecond) + } + + c.T.Logf("activation token not found") + c.T.FailNow() + return "" +} + +func (c *Client) activateUser(token string) { const query = ` - mutation($input: AcceptInvitationInput!) { - acceptInvitation(input: $input) { - membershipEdge { - node { - profile { id } - } + mutation($input: ActivateAccountInput!) { + activateAccount(input: $input) { + profile { + id } } } ` var result struct { - AcceptInvitation struct { - MembershipEdge struct { - Node struct { - Profile struct { - ID string `json:"id"` - } `json:"profile"` - } `json:"node"` - } `json:"membershipEdge"` - } `json:"acceptInvitation"` + ActivateAccount struct { + Profile struct { + ID string `json:"id"` + } `json:"profile"` + } `json:"activateAccount"` } err := c.ExecuteConnect(query, map[string]any{ "input": map[string]any{ - "invitationId": invitationID.String(), + "token": token, }, }, &result) - require.NoError(c.T, err, "acceptInvitation mutation failed") - - profileID, err := gid.ParseGID(result.AcceptInvitation.MembershipEdge.Node.Profile.ID) - require.NoError(c.T, err, "cannot parse profile ID") - - return profileID + require.NoError(c.T, err, "activateAccount mutation failed") } func (c *Client) assumeOrganizationSession() { @@ -385,10 +426,6 @@ func (c *Client) GetUserID() gid.GID { return c.userID } -func (c *Client) GetProfileID() gid.GID { - return c.profileID -} - func (c *Client) GetOrganizationID() gid.GID { return c.organizationID } diff --git a/e2e/internal/testutil/mailpit.go b/e2e/internal/testutil/mailpit.go new file mode 100644 index 000000000..c13b922cc --- /dev/null +++ b/e2e/internal/testutil/mailpit.go @@ -0,0 +1,101 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package testutil + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +type ( + MailpitMessage struct { + ID string `json:"id"` + } + + MailpitSearchResponse struct { + Messages []MailpitMessage `json:"messages"` + } + + MailpitLink struct { + URL string `json:"url"` + } + + MailpitLinkCheckResponse struct { + Links []MailpitLink `json:"links"` + } +) + +func (c *Client) SearchMails(query string) (*MailpitSearchResponse, error) { + req, err := http.NewRequest("GET", c.mailpitBaseURL+"/api/v1/search?query="+url.QueryEscape(query), nil) + if err != nil { + return nil, fmt.Errorf("cannot create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody)) + } + + var searchResp MailpitSearchResponse + if err := json.Unmarshal(respBody, &searchResp); err != nil { + return nil, fmt.Errorf("cannot decode response: %w", err) + } + + return &searchResp, nil +} + +func (c *Client) CheckMessageLinks(messageID string) (*MailpitLinkCheckResponse, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/message/%s/link-check", c.mailpitBaseURL, messageID), nil) + if err != nil { + return nil, fmt.Errorf("cannot create request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody)) + } + + var linkCheckResp MailpitLinkCheckResponse + if err := json.Unmarshal(respBody, &linkCheckResp); err != nil { + return nil, fmt.Errorf("cannot decode response: %w", err) + } + + return &linkCheckResp, nil +} diff --git a/e2e/internal/testutil/testutil.go b/e2e/internal/testutil/testutil.go index 1d290c87a..0fe5f2f63 100644 --- a/e2e/internal/testutil/testutil.go +++ b/e2e/internal/testutil/testutil.go @@ -34,9 +34,10 @@ var ( ) type TestEnv struct { - BaseURL string - cmd *exec.Cmd - done chan error + MailpitBaseURL string + BaseURL string + cmd *exec.Cmd + done chan error } func Setup() { @@ -94,18 +95,24 @@ func Setup() { }() testEnv.BaseURL = "http://localhost:18080" + testEnv.MailpitBaseURL = "http://localhost:8025" ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - if err := waitForServer(ctx, testEnv.BaseURL, 30*time.Second); err != nil { - fmt.Fprintf(os.Stderr, "e2etest: server failed to start: %v\n", err) + if err := waitForServer(ctx, testEnv.BaseURL+"/api/console/v1/graphql", 30*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "e2etest: API server failed to start: %v\n", err) + _ = testEnv.cmd.Process.Kill() + os.Exit(1) + } + if err := waitForServer(ctx, testEnv.MailpitBaseURL+"/api/v1/messages", 30*time.Second); err != nil { + fmt.Fprintf(os.Stderr, "e2etest: MailPit server failed to start: %v\n", err) _ = testEnv.cmd.Process.Kill() os.Exit(1) } }) } -func waitForServer(ctx context.Context, baseURL string, timeout time.Duration) error { +func waitForServer(ctx context.Context, url string, timeout time.Duration) error { deadline := time.Now().Add(timeout) client := &http.Client{Timeout: 2 * time.Second} @@ -116,7 +123,7 @@ func waitForServer(ctx context.Context, baseURL string, timeout time.Duration) e default: } - req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/console/v1/graphql", nil) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } @@ -157,3 +164,10 @@ func GetBaseURL() string { } return testEnv.BaseURL } + +func GetMailpitBaseURL() string { + if testEnv == nil { + return "http://localhost:8025" + } + return testEnv.MailpitBaseURL +} diff --git a/packages/emails/emails.go b/packages/emails/emails.go index 678c4fca1..d2db5ad01 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -319,7 +319,6 @@ func (p *Presenter) RenderInvitation(ctx context.Context, invitationURLPath stri MustParse(vars.BaseURL). AppendPath(invitationURLPath). WithQuery("token", invitationToken). - WithQuery("fullName", vars.RecipientFullName). MustString() data := struct { diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index 1093dce59..e6ccc6a5d 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -414,6 +414,8 @@ WITH profiles AS ( mp.id, mp.identity_id, mp.organization_id, + mp.source, + mp.state, mp.full_name, mp.kind, mp.additional_email_addresses, @@ -438,12 +440,15 @@ SELECT p.identity_id, p.organization_id, i.email_address, + p.source, + p.state, p.full_name, p.kind, p.additional_email_addresses, p.position, p.contract_start_date, p.contract_end_date, + '' AS organization_name, p.created_at, p.updated_at FROM profiles p @@ -516,6 +521,8 @@ WITH profiles AS ( mp.identity_id, mp.organization_id, mp.full_name, + mp.source, + mp.state, mp.kind, mp.additional_email_addresses, mp.position, @@ -539,12 +546,15 @@ SELECT p.identity_id, p.organization_id, i.email_address, + p.source, + p.state, p.full_name, p.kind, p.additional_email_addresses, p.position, p.contract_start_date, p.contract_end_date, + '' AS organization_name, p.created_at, p.updated_at FROM profiles p diff --git a/pkg/coredata/migrations/20260212T163603Z.sql b/pkg/coredata/migrations/20260212T163603Z.sql index fb8889d29..ab4d1d704 100644 --- a/pkg/coredata/migrations/20260212T163603Z.sql +++ b/pkg/coredata/migrations/20260212T163603Z.sql @@ -184,6 +184,27 @@ WHERE pi.organization_id = i.organization_id AND pi.email_address = i.email; +-- Delete orphan invitations: e.g. expired invitations that were never accepted +WITH orphan_invitations AS ( + SELECT + inv.id + FROM + iam_invitations inv + LEFT JOIN identities i ON i.email_address = inv.email + LEFT JOIN iam_membership_profiles p ON p.identity_id = i.id + WHERE + i.id IS NULL OR p.id IS NULL +) +DELETE FROM + iam_invitations +WHERE + id IN ( + SELECT + id + FROM + orphan_invitations + ); + ALTER TABLE iam_invitations ALTER COLUMN @@ -197,4 +218,4 @@ ALTER COLUMN ALTER COLUMN role DROP NOT NULL, ALTER COLUMN - full_name DROP NOT NULL; + full_name DROP NOT NULL; \ No newline at end of file diff --git a/pkg/iam/iam_actions.go b/pkg/iam/iam_actions.go index 7569b7387..9d883360b 100644 --- a/pkg/iam/iam_actions.go +++ b/pkg/iam/iam_actions.go @@ -52,11 +52,13 @@ const ( ActionMembershipRoleSetOwner = "iam:membership-role:set-owner" // Membership Profile actions - ActionMembershipProfileGet = "iam:membership-profile:get" - ActionMembershipProfileList = "iam:membership-profile:list" - ActionMembershipProfileCreate = "iam:membership-profile:create" - ActionMembershipProfileUpdate = "iam:membership-profile:update" - ActionMembershipProfileDelete = "iam:membership-profile:delete" + ActionMembershipProfileGet = "iam:membership-profile:get" + ActionMembershipProfileList = "iam:membership-profile:list" + ActionMembershipProfileCreate = "iam:membership-profile:create" + ActionMembershipProfileUpdate = "iam:membership-profile:update" + ActionMembershipProfileDelete = "iam:membership-profile:delete" + ActionMembershipProfileActivate = "iam:membership-profile:activate" + ActionMembershipProfileDeactivate = "iam:membership-profile:deactivate" // Personal API Key actions ActionPersonalAPIKeyCreate = "iam:personal-api-key:create" diff --git a/pkg/iam/iam_policies.go b/pkg/iam/iam_policies.go index 7ac624fbf..b5022077f 100644 --- a/pkg/iam/iam_policies.go +++ b/pkg/iam/iam_policies.go @@ -167,6 +167,8 @@ var IAMOwnerPolicy = policy.NewPolicy( ActionMembershipProfileCreate, ActionMembershipProfileUpdate, ActionMembershipProfileDelete, + ActionMembershipProfileActivate, + ActionMembershipProfileDeactivate, ). WithSID("full-membership-profile-access"). When(policy.Equals("principal.organization_id", "resource.organization_id")), @@ -248,6 +250,8 @@ var IAMAdminPolicy = policy.NewPolicy( ActionMembershipProfileCreate, ActionMembershipProfileUpdate, ActionMembershipProfileDelete, + ActionMembershipProfileActivate, + ActionMembershipProfileDeactivate, ). WithSID("membership-profile-admin-access"). When(policy.Equals("principal.organization_id", "resource.organization_id")), diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index aa398cfdc..958a9b9e3 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -427,7 +427,7 @@ func (s *OrganizationService) CreateOrganization( ctx context.Context, identityID gid.GID, req *CreateOrganizationRequest, -) (*coredata.Organization, *coredata.Membership, error) { +) (*coredata.Organization, *coredata.MembershipProfile, error) { if err := req.Validate(); err != nil { return nil, nil, fmt.Errorf("invalid request: %w", err) } @@ -639,7 +639,7 @@ func (s *OrganizationService) CreateOrganization( return nil, nil, fmt.Errorf("cannot insert organization: %w", err) } - return organization, membership, nil + return organization, profile, nil } func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizationID gid.GID, req *UpdateOrganizationRequest) (*coredata.Organization, error) { @@ -973,6 +973,41 @@ func (s *OrganizationService) UpdateUser(ctx context.Context, req *UpdateUserReq return profile, nil } +func (s *OrganizationService) UpdateUserState( + ctx context.Context, + userID gid.GID, + state coredata.ProfileState, +) (*coredata.MembershipProfile, error) { + var ( + scope = coredata.NewScopeFromObjectID(userID) + profile = &coredata.MembershipProfile{} + ) + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := profile.LoadByID(ctx, conn, scope, userID); err != nil { + return fmt.Errorf("cannot load profile: %w", err) + } + + profile.State = state + profile.UpdatedAt = time.Now() + + if err := profile.Update(ctx, conn, scope); err != nil { + return fmt.Errorf("cannot update profile: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return profile, nil +} + func (s *OrganizationService) GetProfile(ctx context.Context, profileID gid.GID) (*coredata.MembershipProfile, error) { profile := &coredata.MembershipProfile{} diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index a2b859a4f..20576c2f1 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -55,7 +55,7 @@ type Mutation { signOut: SignOutPayload @session(required: PRESENT) activateAccount( input: ActivateAccountInput! - ): ActivateAccountPayload @session(required: NONE) + ): ActivateAccountPayload @session(required: OPTIONAL) forgotPassword(input: ForgotPasswordInput!): ForgotPasswordPayload @session(required: NONE) resetPassword(input: ResetPasswordInput!): ResetPasswordPayload @@ -98,6 +98,7 @@ type Mutation { @session(required: PRESENT) inviteUser(input: InviteUserInput!): InviteUserPayload @session(required: PRESENT) + deactivateUser(input: DeactivateUserInput!): DeactivateUserPayload updateUser(input: UpdateUserInput!): UpdateUserPayload! updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload! removeUser(input: RemoveUserInput!): RemoveUserPayload @@ -719,6 +720,16 @@ input InviteUserInput { profileId: ID! } +input ActivateUserInput { + organizationId: ID! + profileId: ID! +} + +input DeactivateUserInput { + organizationId: ID! + profileId: ID! +} + input UpdateUserInput { id: ID! fullName: String! @@ -740,15 +751,6 @@ input RemoveUserInput { profileId: ID! } -input AcceptInvitationInput { - invitationId: ID! -} - -input DeleteInvitationInput { - organizationId: ID! - invitationId: ID! -} - input CreateSAMLConfigurationInput { organizationId: ID! emailDomain: String! @@ -861,7 +863,7 @@ type RevokePersonalAPIKeyPayload { type CreateOrganizationPayload { organization: Organization - membership: Membership! + profile: Profile! } type UpdateOrganizationPayload { @@ -884,6 +886,10 @@ type InviteUserPayload { invitationEdge: InvitationEdge! } +type DeactivateUserPayload { + success: Boolean! +} + type UpdateUserPayload { profile: Profile! } @@ -896,15 +902,6 @@ type RemoveUserPayload { deletedProfileId: ID! } -type AcceptInvitationPayload { - membership: Membership! - invitation: Invitation! -} - -type DeleteInvitationPayload { - deletedInvitationId: ID! -} - type CreateSAMLConfigurationPayload { samlConfigurationEdge: SAMLConfigurationEdge! } diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 8521f1c14..6ea67636c 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -74,11 +74,6 @@ type DirectiveRoot struct { } type ComplexityRoot struct { - AcceptInvitationPayload struct { - Invitation func(childComplexity int) int - Membership func(childComplexity int) int - } - ActivateAccountPayload struct { CreatePasswordToken func(childComplexity int) int Profile func(childComplexity int) int @@ -105,8 +100,8 @@ type ComplexityRoot struct { } CreateOrganizationPayload struct { - Membership func(childComplexity int) int Organization func(childComplexity int) int + Profile func(childComplexity int) int } CreatePersonalAPIKeyPayload struct { @@ -128,8 +123,8 @@ type ComplexityRoot struct { ProfileEdge func(childComplexity int) int } - DeleteInvitationPayload struct { - DeletedInvitationID func(childComplexity int) int + DeactivateUserPayload struct { + Success func(childComplexity int) int } DeleteOrganizationHorizontalLogoPayload struct { @@ -209,6 +204,7 @@ type ComplexityRoot struct { CreateSAMLConfiguration func(childComplexity int, input types.CreateSAMLConfigurationInput) int CreateSCIMConfiguration func(childComplexity int, input types.CreateSCIMConfigurationInput) int CreateUser func(childComplexity int, input types.CreateUserInput) int + DeactivateUser func(childComplexity int, input types.DeactivateUserInput) int DeleteOrganization func(childComplexity int, input types.DeleteOrganizationInput) int DeleteOrganizationHorizontalLogo func(childComplexity int, input types.DeleteOrganizationHorizontalLogoInput) int DeleteSAMLConfiguration func(childComplexity int, input types.DeleteSAMLConfigurationInput) int @@ -539,6 +535,7 @@ type MutationResolver interface { DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) CreateUser(ctx context.Context, input types.CreateUserInput) (*types.CreateUserPayload, error) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) + DeactivateUser(ctx context.Context, input types.DeactivateUserInput) (*types.DeactivateUserPayload, error) UpdateUser(ctx context.Context, input types.UpdateUserInput) (*types.UpdateUserPayload, error) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) @@ -638,19 +635,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin _ = ec switch typeName + "." + field { - case "AcceptInvitationPayload.invitation": - if e.complexity.AcceptInvitationPayload.Invitation == nil { - break - } - - return e.complexity.AcceptInvitationPayload.Invitation(childComplexity), true - case "AcceptInvitationPayload.membership": - if e.complexity.AcceptInvitationPayload.Membership == nil { - break - } - - return e.complexity.AcceptInvitationPayload.Membership(childComplexity), true - case "ActivateAccountPayload.createPasswordToken": if e.complexity.ActivateAccountPayload.CreatePasswordToken == nil { break @@ -721,18 +705,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Connector.UpdatedAt(childComplexity), true - case "CreateOrganizationPayload.membership": - if e.complexity.CreateOrganizationPayload.Membership == nil { - break - } - - return e.complexity.CreateOrganizationPayload.Membership(childComplexity), true case "CreateOrganizationPayload.organization": if e.complexity.CreateOrganizationPayload.Organization == nil { break } return e.complexity.CreateOrganizationPayload.Organization(childComplexity), true + case "CreateOrganizationPayload.profile": + if e.complexity.CreateOrganizationPayload.Profile == nil { + break + } + + return e.complexity.CreateOrganizationPayload.Profile(childComplexity), true case "CreatePersonalAPIKeyPayload.personalAPIKeyEdge": if e.complexity.CreatePersonalAPIKeyPayload.PersonalAPIKeyEdge == nil { @@ -780,12 +764,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.CreateUserPayload.ProfileEdge(childComplexity), true - case "DeleteInvitationPayload.deletedInvitationId": - if e.complexity.DeleteInvitationPayload.DeletedInvitationID == nil { + case "DeactivateUserPayload.success": + if e.complexity.DeactivateUserPayload.Success == nil { break } - return e.complexity.DeleteInvitationPayload.DeletedInvitationID(childComplexity), true + return e.complexity.DeactivateUserPayload.Success(childComplexity), true case "DeleteOrganizationHorizontalLogoPayload.organization": if e.complexity.DeleteOrganizationHorizontalLogoPayload.Organization == nil { @@ -1131,6 +1115,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.CreateUser(childComplexity, args["input"].(types.CreateUserInput)), true + case "Mutation.deactivateUser": + if e.complexity.Mutation.DeactivateUser == nil { + break + } + + args, err := ec.field_Mutation_deactivateUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeactivateUser(childComplexity, args["input"].(types.DeactivateUserInput)), true case "Mutation.deleteOrganization": if e.complexity.Mutation.DeleteOrganization == nil { break @@ -2346,8 +2341,8 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { opCtx := graphql.GetOperationContext(ctx) ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( - ec.unmarshalInputAcceptInvitationInput, ec.unmarshalInputActivateAccountInput, + ec.unmarshalInputActivateUserInput, ec.unmarshalInputAssumeOrganizationSessionInput, ec.unmarshalInputChangeEmailInput, ec.unmarshalInputChangePasswordInput, @@ -2356,7 +2351,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreateSAMLConfigurationInput, ec.unmarshalInputCreateSCIMConfigurationInput, ec.unmarshalInputCreateUserInput, - ec.unmarshalInputDeleteInvitationInput, + ec.unmarshalInputDeactivateUserInput, ec.unmarshalInputDeleteOrganizationHorizontalLogoInput, ec.unmarshalInputDeleteOrganizationInput, ec.unmarshalInputDeleteSAMLConfigurationInput, @@ -2536,7 +2531,7 @@ type Mutation { signOut: SignOutPayload @session(required: PRESENT) activateAccount( input: ActivateAccountInput! - ): ActivateAccountPayload @session(required: NONE) + ): ActivateAccountPayload @session(required: OPTIONAL) forgotPassword(input: ForgotPasswordInput!): ForgotPasswordPayload @session(required: NONE) resetPassword(input: ResetPasswordInput!): ResetPasswordPayload @@ -2579,6 +2574,7 @@ type Mutation { @session(required: PRESENT) inviteUser(input: InviteUserInput!): InviteUserPayload @session(required: PRESENT) + deactivateUser(input: DeactivateUserInput!): DeactivateUserPayload updateUser(input: UpdateUserInput!): UpdateUserPayload! updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload! removeUser(input: RemoveUserInput!): RemoveUserPayload @@ -3200,6 +3196,16 @@ input InviteUserInput { profileId: ID! } +input ActivateUserInput { + organizationId: ID! + profileId: ID! +} + +input DeactivateUserInput { + organizationId: ID! + profileId: ID! +} + input UpdateUserInput { id: ID! fullName: String! @@ -3221,15 +3227,6 @@ input RemoveUserInput { profileId: ID! } -input AcceptInvitationInput { - invitationId: ID! -} - -input DeleteInvitationInput { - organizationId: ID! - invitationId: ID! -} - input CreateSAMLConfigurationInput { organizationId: ID! emailDomain: String! @@ -3342,7 +3339,7 @@ type RevokePersonalAPIKeyPayload { type CreateOrganizationPayload { organization: Organization - membership: Membership! + profile: Profile! } type UpdateOrganizationPayload { @@ -3365,6 +3362,10 @@ type InviteUserPayload { invitationEdge: InvitationEdge! } +type DeactivateUserPayload { + success: Boolean! +} + type UpdateUserPayload { profile: Profile! } @@ -3377,15 +3378,6 @@ type RemoveUserPayload { deletedProfileId: ID! } -type AcceptInvitationPayload { - membership: Membership! - invitation: Invitation! -} - -type DeleteInvitationPayload { - deletedInvitationId: ID! -} - type CreateSAMLConfigurationPayload { samlConfigurationEdge: SAMLConfigurationEdge! } @@ -3731,6 +3723,17 @@ func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_deactivateUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNDeactivateUserInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeactivateUserInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteOrganizationHorizontalLogo_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4232,94 +4235,6 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** -func (ec *executionContext) _AcceptInvitationPayload_membership(ctx context.Context, field graphql.CollectedField, obj *types.AcceptInvitationPayload) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - ec.fieldContext_AcceptInvitationPayload_membership, - func(ctx context.Context) (any, error) { - return obj.Membership, nil - }, - nil, - ec.marshalNMembership2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembership, - true, - true, - ) -} - -func (ec *executionContext) fieldContext_AcceptInvitationPayload_membership(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "AcceptInvitationPayload", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_Membership_id(ctx, field) - case "createdAt": - return ec.fieldContext_Membership_createdAt(ctx, field) - case "role": - return ec.fieldContext_Membership_role(ctx, field) - case "lastSession": - return ec.fieldContext_Membership_lastSession(ctx, field) - case "permission": - return ec.fieldContext_Membership_permission(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Membership", field.Name) - }, - } - return fc, nil -} - -func (ec *executionContext) _AcceptInvitationPayload_invitation(ctx context.Context, field graphql.CollectedField, obj *types.AcceptInvitationPayload) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - ec.fieldContext_AcceptInvitationPayload_invitation, - func(ctx context.Context) (any, error) { - return obj.Invitation, nil - }, - nil, - ec.marshalNInvitation2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐInvitation, - true, - true, - ) -} - -func (ec *executionContext) fieldContext_AcceptInvitationPayload_invitation(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "AcceptInvitationPayload", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_Invitation_id(ctx, field) - case "expiresAt": - return ec.fieldContext_Invitation_expiresAt(ctx, field) - case "acceptedAt": - return ec.fieldContext_Invitation_acceptedAt(ctx, field) - case "createdAt": - return ec.fieldContext_Invitation_createdAt(ctx, field) - case "status": - return ec.fieldContext_Invitation_status(ctx, field) - case "user": - return ec.fieldContext_Invitation_user(ctx, field) - case "organization": - return ec.fieldContext_Invitation_organization(ctx, field) - case "permission": - return ec.fieldContext_Invitation_permission(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Invitation", field.Name) - }, - } - return fc, nil -} - func (ec *executionContext) _ActivateAccountPayload_createPasswordToken(ctx context.Context, field graphql.CollectedField, obj *types.ActivateAccountPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -4737,23 +4652,23 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_organization( return fc, nil } -func (ec *executionContext) _CreateOrganizationPayload_membership(ctx context.Context, field graphql.CollectedField, obj *types.CreateOrganizationPayload) (ret graphql.Marshaler) { +func (ec *executionContext) _CreateOrganizationPayload_profile(ctx context.Context, field graphql.CollectedField, obj *types.CreateOrganizationPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_CreateOrganizationPayload_membership, + ec.fieldContext_CreateOrganizationPayload_profile, func(ctx context.Context) (any, error) { - return obj.Membership, nil + return obj.Profile, nil }, nil, - ec.marshalNMembership2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembership, + ec.marshalNProfile2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfile, true, true, ) } -func (ec *executionContext) fieldContext_CreateOrganizationPayload_membership(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_CreateOrganizationPayload_profile(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "CreateOrganizationPayload", Field: field, @@ -4762,17 +4677,41 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_membership(_ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_Membership_id(ctx, field) + return ec.fieldContext_Profile_id(ctx, field) + case "fullName": + return ec.fieldContext_Profile_fullName(ctx, field) + case "emailAddress": + return ec.fieldContext_Profile_emailAddress(ctx, field) + case "source": + return ec.fieldContext_Profile_source(ctx, field) + case "state": + return ec.fieldContext_Profile_state(ctx, field) + case "additionalEmailAddresses": + return ec.fieldContext_Profile_additionalEmailAddresses(ctx, field) + case "kind": + return ec.fieldContext_Profile_kind(ctx, field) + case "position": + return ec.fieldContext_Profile_position(ctx, field) + case "contractStartDate": + return ec.fieldContext_Profile_contractStartDate(ctx, field) + case "contractEndDate": + return ec.fieldContext_Profile_contractEndDate(ctx, field) case "createdAt": - return ec.fieldContext_Membership_createdAt(ctx, field) - case "role": - return ec.fieldContext_Membership_role(ctx, field) - case "lastSession": - return ec.fieldContext_Membership_lastSession(ctx, field) + return ec.fieldContext_Profile_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Profile_updatedAt(ctx, field) + case "identity": + return ec.fieldContext_Profile_identity(ctx, field) + case "organization": + return ec.fieldContext_Profile_organization(ctx, field) + case "membership": + return ec.fieldContext_Profile_membership(ctx, field) + case "pendingInvitations": + return ec.fieldContext_Profile_pendingInvitations(ctx, field) case "permission": - return ec.fieldContext_Membership_permission(ctx, field) + return ec.fieldContext_Profile_permission(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type Membership", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Profile", field.Name) }, } return fc, nil @@ -5037,30 +4976,30 @@ func (ec *executionContext) fieldContext_CreateUserPayload_profileEdge(_ context return fc, nil } -func (ec *executionContext) _DeleteInvitationPayload_deletedInvitationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteInvitationPayload) (ret graphql.Marshaler) { +func (ec *executionContext) _DeactivateUserPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.DeactivateUserPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_DeleteInvitationPayload_deletedInvitationId, + ec.fieldContext_DeactivateUserPayload_success, func(ctx context.Context) (any, error) { - return obj.DeletedInvitationID, nil + return obj.Success, nil }, nil, - ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + ec.marshalNBoolean2bool, true, true, ) } -func (ec *executionContext) fieldContext_DeleteInvitationPayload_deletedInvitationId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_DeactivateUserPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "DeleteInvitationPayload", + Object: "DeactivateUserPayload", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + return nil, errors.New("field of type Boolean does not have child fields") }, } return fc, nil @@ -6600,7 +6539,7 @@ func (ec *executionContext) _Mutation_activateAccount(ctx context.Context, field directive0 := next directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") + required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "OPTIONAL") if err != nil { var zeroVal *types.ActivateAccountPayload return zeroVal, err @@ -7316,8 +7255,8 @@ func (ec *executionContext) fieldContext_Mutation_createOrganization(ctx context switch field.Name { case "organization": return ec.fieldContext_CreateOrganizationPayload_organization(ctx, field) - case "membership": - return ec.fieldContext_CreateOrganizationPayload_membership(ctx, field) + case "profile": + return ec.fieldContext_CreateOrganizationPayload_profile(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type CreateOrganizationPayload", field.Name) }, @@ -7651,6 +7590,51 @@ func (ec *executionContext) fieldContext_Mutation_inviteUser(ctx context.Context return fc, nil } +func (ec *executionContext) _Mutation_deactivateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_deactivateUser, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().DeactivateUser(ctx, fc.Args["input"].(types.DeactivateUserInput)) + }, + nil, + ec.marshalODeactivateUserPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeactivateUserPayload, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Mutation_deactivateUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "success": + return ec.fieldContext_DeactivateUserPayload_success(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DeactivateUserPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deactivateUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_updateUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -15145,33 +15129,6 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field // region **************************** input.gotpl ***************************** -func (ec *executionContext) unmarshalInputAcceptInvitationInput(ctx context.Context, obj any) (types.AcceptInvitationInput, error) { - var it types.AcceptInvitationInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"invitationId"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "invitationId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("invitationId")) - data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) - if err != nil { - return it, err - } - it.InvitationID = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputActivateAccountInput(ctx context.Context, obj any) (types.ActivateAccountInput, error) { var it types.ActivateAccountInput asMap := map[string]any{} @@ -15199,6 +15156,40 @@ func (ec *executionContext) unmarshalInputActivateAccountInput(ctx context.Conte return it, nil } +func (ec *executionContext) unmarshalInputActivateUserInput(ctx context.Context, obj any) (types.ActivateUserInput, error) { + var it types.ActivateUserInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"organizationId", "profileId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "organizationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OrganizationID = data + case "profileId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("profileId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.ProfileID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputAssumeOrganizationSessionInput(ctx context.Context, obj any) (types.AssumeOrganizationSessionInput, error) { var it types.AssumeOrganizationSessionInput asMap := map[string]any{} @@ -15562,14 +15553,14 @@ func (ec *executionContext) unmarshalInputCreateUserInput(ctx context.Context, o return it, nil } -func (ec *executionContext) unmarshalInputDeleteInvitationInput(ctx context.Context, obj any) (types.DeleteInvitationInput, error) { - var it types.DeleteInvitationInput +func (ec *executionContext) unmarshalInputDeactivateUserInput(ctx context.Context, obj any) (types.DeactivateUserInput, error) { + var it types.DeactivateUserInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v } - fieldsInOrder := [...]string{"organizationId", "invitationId"} + fieldsInOrder := [...]string{"organizationId", "profileId"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -15583,13 +15574,13 @@ func (ec *executionContext) unmarshalInputDeleteInvitationInput(ctx context.Cont return it, err } it.OrganizationID = data - case "invitationId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("invitationId")) + case "profileId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("profileId")) data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) if err != nil { return it, err } - it.InvitationID = data + it.ProfileID = data } } @@ -16704,50 +16695,6 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj // region **************************** object.gotpl **************************** -var acceptInvitationPayloadImplementors = []string{"AcceptInvitationPayload"} - -func (ec *executionContext) _AcceptInvitationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.AcceptInvitationPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, acceptInvitationPayloadImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("AcceptInvitationPayload") - case "membership": - out.Values[i] = ec._AcceptInvitationPayload_membership(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "invitation": - out.Values[i] = ec._AcceptInvitationPayload_invitation(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - var activateAccountPayloadImplementors = []string{"ActivateAccountPayload"} func (ec *executionContext) _ActivateAccountPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ActivateAccountPayload) graphql.Marshaler { @@ -17006,8 +16953,8 @@ func (ec *executionContext) _CreateOrganizationPayload(ctx context.Context, sel out.Values[i] = graphql.MarshalString("CreateOrganizationPayload") case "organization": out.Values[i] = ec._CreateOrganizationPayload_organization(ctx, field, obj) - case "membership": - out.Values[i] = ec._CreateOrganizationPayload_membership(ctx, field, obj) + case "profile": + out.Values[i] = ec._CreateOrganizationPayload_profile(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -17202,19 +17149,19 @@ func (ec *executionContext) _CreateUserPayload(ctx context.Context, sel ast.Sele return out } -var deleteInvitationPayloadImplementors = []string{"DeleteInvitationPayload"} +var deactivateUserPayloadImplementors = []string{"DeactivateUserPayload"} -func (ec *executionContext) _DeleteInvitationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteInvitationPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, deleteInvitationPayloadImplementors) +func (ec *executionContext) _DeactivateUserPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeactivateUserPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deactivateUserPayloadImplementors) out := graphql.NewFieldSet(fields) deferred := make(map[string]*graphql.FieldSet) for i, field := range fields { switch field.Name { case "__typename": - out.Values[i] = graphql.MarshalString("DeleteInvitationPayload") - case "deletedInvitationId": - out.Values[i] = ec._DeleteInvitationPayload_deletedInvitationId(ctx, field, obj) + out.Values[i] = graphql.MarshalString("DeactivateUserPayload") + case "success": + out.Values[i] = ec._DeactivateUserPayload_success(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -18206,6 +18153,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_inviteUser(ctx, field) }) + case "deactivateUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deactivateUser(ctx, field) + }) case "updateUser": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_updateUser(ctx, field) @@ -21721,6 +21672,11 @@ func (ec *executionContext) marshalNDatetime2timeᚐTime(ctx context.Context, se return res } +func (ec *executionContext) unmarshalNDeactivateUserInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeactivateUserInput(ctx context.Context, v any) (types.DeactivateUserInput, error) { + res, err := ec.unmarshalInputDeactivateUserInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNDeleteOrganizationHorizontalLogoInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoInput(ctx context.Context, v any) (types.DeleteOrganizationHorizontalLogoInput, error) { res, err := ec.unmarshalInputDeleteOrganizationHorizontalLogoInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -23199,6 +23155,13 @@ func (ec *executionContext) marshalODatetime2ᚖtimeᚐTime(ctx context.Context, return res } +func (ec *executionContext) marshalODeactivateUserPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeactivateUserPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeactivateUserPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._DeactivateUserPayload(ctx, sel, v) +} + func (ec *executionContext) marshalODeleteOrganizationHorizontalLogoPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteOrganizationHorizontalLogoPayload) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index f2fe5262d..886b6c0e2 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -25,15 +25,6 @@ type Node interface { GetID() gid.GID } -type AcceptInvitationInput struct { - InvitationID gid.GID `json:"invitationId"` -} - -type AcceptInvitationPayload struct { - Membership *Membership `json:"membership"` - Invitation *Invitation `json:"invitation"` -} - type ActivateAccountInput struct { Token string `json:"token"` } @@ -43,6 +34,11 @@ type ActivateAccountPayload struct { Profile *Profile `json:"profile,omitempty"` } +type ActivateUserInput struct { + OrganizationID gid.GID `json:"organizationId"` + ProfileID gid.GID `json:"profileId"` +} + type AssumeOrganizationSessionInput struct { OrganizationID gid.GID `json:"organizationId"` Continue string `json:"continue"` @@ -89,7 +85,7 @@ type CreateOrganizationInput struct { type CreateOrganizationPayload struct { Organization *Organization `json:"organization,omitempty"` - Membership *Membership `json:"membership"` + Profile *Profile `json:"profile"` } type CreatePersonalAPIKeyInput struct { @@ -143,13 +139,13 @@ type CreateUserPayload struct { ProfileEdge *ProfileEdge `json:"profileEdge"` } -type DeleteInvitationInput struct { +type DeactivateUserInput struct { OrganizationID gid.GID `json:"organizationId"` - InvitationID gid.GID `json:"invitationId"` + ProfileID gid.GID `json:"profileId"` } -type DeleteInvitationPayload struct { - DeletedInvitationID gid.GID `json:"deletedInvitationId"` +type DeactivateUserPayload struct { + Success bool `json:"success"` } type DeleteOrganizationHorizontalLogoInput struct { diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 8f31cde77..9a01a1fa1 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -718,7 +718,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C Size: input.HorizontalLogoFile.Size, } } - organization, membership, err := r.iam.OrganizationService.CreateOrganization( + organization, profile, err := r.iam.OrganizationService.CreateOrganization( ctx, identity.ID, &iam.CreateOrganizationRequest{ @@ -734,7 +734,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C return &types.CreateOrganizationPayload{ Organization: types.NewOrganization(organization), - Membership: types.NewMembership(membership), + Profile: types.NewProfile(profile), }, nil } @@ -883,6 +883,28 @@ func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUse }, nil } +// DeactivateUser is the resolver for the deactivateUser field. +func (r *mutationResolver) DeactivateUser(ctx context.Context, input types.DeactivateUserInput) (*types.DeactivateUserPayload, error) { + if err := r.authorize(ctx, input.ProfileID, iam.ActionMembershipProfileDeactivate); err != nil { + return nil, err + } + + _, err := r.iam.OrganizationService.UpdateUserState( + ctx, + input.ProfileID, + coredata.ProfileStateInactive, + ) + + if err != nil { + r.logger.ErrorCtx(ctx, "cannot deactivate profile", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.DeactivateUserPayload{ + Success: true, + }, nil +} + // UpdateUser is the resolver for the updateUser field. func (r *mutationResolver) UpdateUser(ctx context.Context, input types.UpdateUserInput) (*types.UpdateUserPayload, error) { if err := r.authorize(ctx, input.ID, iam.ActionMembershipProfileUpdate); err != nil {