Files
probo/e2e/console/access_review_test.go
Bryan Frimin 2dc34df4e1 Fix access review e2e tests after API refactor
The campaign source node test queried entries before starting the
campaign, and the lifecycle test used the old accessEntry field name
instead of accessReviewEntry.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-06-15 16:49:08 +02:00

1589 lines
41 KiB
Go

// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// 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 console_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
const testCsvData = "email,full_name,role,job_title,is_admin,active,mfa_status,auth_method,last_login,account_created_at,external_id\njane@example.com,Jane Smith,admin,CTO,true,true,ENABLED,SSO,2026-01-15T00:00:00Z,2024-06-01T00:00:00Z,ext-jane"
func TestAccessReviewSource_Create(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
t.Run("with name only", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewSourceInput!) {
createAccessReviewSource(input: $input) {
accessReviewSourceEdge {
node {
id
name
createdAt
updatedAt
}
}
}
}
`
var result struct {
CreateAccessReviewSource struct {
AccessReviewSourceEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
} `json:"node"`
} `json:"accessReviewSourceEdge"`
} `json:"createAccessReviewSource"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "Slack",
},
}, &result)
require.NoError(t, err)
node := result.CreateAccessReviewSource.AccessReviewSourceEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "Slack", node.Name)
assert.NotEmpty(t, node.CreatedAt)
})
t.Run("with csv data", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewSourceInput!) {
createAccessReviewSource(input: $input) {
accessReviewSourceEdge {
node {
id
name
csvData
}
}
}
}
`
var result struct {
CreateAccessReviewSource struct {
AccessReviewSourceEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
CsvData *string `json:"csvData"`
} `json:"node"`
} `json:"accessReviewSourceEdge"`
} `json:"createAccessReviewSource"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "CSV Import",
"csvData": testCsvData,
},
}, &result)
require.NoError(t, err)
node := result.CreateAccessReviewSource.AccessReviewSourceEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "CSV Import", node.Name)
require.NotNil(t, node.CsvData)
assert.Contains(t, *node.CsvData, "jane@example.com")
})
}
func TestAccessReviewSource_Update(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Original Source").
Create()
const query = `
mutation($input: UpdateAccessReviewSourceInput!) {
updateAccessReviewSource(input: $input) {
accessReviewSource {
id
name
}
}
}
`
var result struct {
UpdateAccessReviewSource struct {
AccessReviewSource struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"accessReviewSource"`
} `json:"updateAccessReviewSource"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewSourceId": sourceID,
"name": "Updated Source",
},
}, &result)
require.NoError(t, err)
assert.Equal(t, sourceID, result.UpdateAccessReviewSource.AccessReviewSource.ID)
assert.Equal(t, "Updated Source", result.UpdateAccessReviewSource.AccessReviewSource.Name)
}
func TestAccessReviewSource_Delete(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Source to Delete").
Create()
const query = `
mutation($input: DeleteAccessReviewSourceInput!) {
deleteAccessReviewSource(input: $input) {
deletedAccessReviewSourceId
}
}
`
var result struct {
DeleteAccessReviewSource struct {
DeletedAccessReviewSourceID string `json:"deletedAccessReviewSourceId"`
} `json:"deleteAccessReviewSource"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewSourceId": sourceID,
},
}, &result)
require.NoError(t, err)
assert.Equal(t, sourceID, result.DeleteAccessReviewSource.DeletedAccessReviewSourceID)
}
func TestAccessReviewSource_List(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
for _, name := range []string{"Slack", "GitHub", "Google Workspace"} {
factory.NewAccessReviewSource(owner, orgID).WithName(name).Create()
}
const query = `
query($id: ID!) {
node(id: $id) {
... on Organization {
accessReviewSources(first: 10) {
edges {
node {
id
name
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
AccessReviewSources struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"accessReviewSources"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{"id": orgID}, &result)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AccessReviewSources.TotalCount, 3)
}
func TestAccessReviewCampaign_Create(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
t.Run("with name only", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewCampaignInput!) {
createAccessReviewCampaign(input: $input) {
accessReviewCampaignEdge {
node {
id
name
status
createdAt
updatedAt
}
}
}
}
`
var result struct {
CreateAccessReviewCampaign struct {
AccessReviewCampaignEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
} `json:"node"`
} `json:"accessReviewCampaignEdge"`
} `json:"createAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "Q1 2026 Review",
},
}, &result)
require.NoError(t, err)
node := result.CreateAccessReviewCampaign.AccessReviewCampaignEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "Q1 2026 Review", node.Name)
assert.Equal(t, "DRAFT", node.Status)
assert.NotEmpty(t, node.CreatedAt)
})
t.Run("with access sources", func(t *testing.T) {
t.Parallel()
source1ID := factory.NewAccessReviewSource(owner, orgID).
WithName("Slack Source").
Create()
source2ID := factory.NewAccessReviewSource(owner, orgID).
WithName("GitHub Source").
Create()
const query = `
mutation($input: CreateAccessReviewCampaignInput!) {
createAccessReviewCampaign(input: $input) {
accessReviewCampaignEdge {
node {
id
name
sources {
id
name
}
}
}
}
}
`
var result struct {
CreateAccessReviewCampaign struct {
AccessReviewCampaignEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
CampaignSources []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"sources"`
} `json:"node"`
} `json:"accessReviewCampaignEdge"`
} `json:"createAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "Campaign with Sources",
"accessReviewSourceIds": []string{source1ID, source2ID},
},
}, &result)
require.NoError(t, err)
node := result.CreateAccessReviewCampaign.AccessReviewCampaignEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "Campaign with Sources", node.Name)
assert.Len(t, node.CampaignSources, 2)
})
t.Run("with framework controls", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewCampaignInput!) {
createAccessReviewCampaign(input: $input) {
accessReviewCampaignEdge {
node {
id
name
frameworkControls
}
}
}
}
`
var result struct {
CreateAccessReviewCampaign struct {
AccessReviewCampaignEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
FrameworkControls []string `json:"frameworkControls"`
} `json:"node"`
} `json:"accessReviewCampaignEdge"`
} `json:"createAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "SOC2 Campaign",
"frameworkControls": []string{"CC6.1", "CC6.2"},
},
}, &result)
require.NoError(t, err)
node := result.CreateAccessReviewCampaign.AccessReviewCampaignEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "SOC2 Campaign", node.Name)
assert.Contains(t, node.FrameworkControls, "CC6.1")
assert.Contains(t, node.FrameworkControls, "CC6.2")
})
}
func TestAccessReviewCampaign_Update(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Original Campaign").
Create()
const query = `
mutation($input: UpdateAccessReviewCampaignInput!) {
updateAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
name
}
}
}
`
var result struct {
UpdateAccessReviewCampaign struct {
AccessReviewCampaign struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"accessReviewCampaign"`
} `json:"updateAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
"name": "Renamed Campaign",
},
}, &result)
require.NoError(t, err)
assert.Equal(t, campaignID, result.UpdateAccessReviewCampaign.AccessReviewCampaign.ID)
assert.Equal(t, "Renamed Campaign", result.UpdateAccessReviewCampaign.AccessReviewCampaign.Name)
}
func TestAccessReviewCampaign_Delete(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Campaign to Delete").
Create()
const query = `
mutation($input: DeleteAccessReviewCampaignInput!) {
deleteAccessReviewCampaign(input: $input) {
deletedAccessReviewCampaignId
}
}
`
var result struct {
DeleteAccessReviewCampaign struct {
DeletedAccessReviewCampaignID string `json:"deletedAccessReviewCampaignId"`
} `json:"deleteAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, &result)
require.NoError(t, err)
assert.Equal(t, campaignID, result.DeleteAccessReviewCampaign.DeletedAccessReviewCampaignID)
}
// TestAccessReviewCampaign_DeleteRemovesFromListAndNode guards the contract
// the console frontend relies on after deleting a campaign: the campaign must
// disappear from the organization's `accessReviewCampaigns` connection, and a
// `node(id:)` lookup on the deleted GID must surface a NOT_FOUND error rather
// than partial data. Without this contract the cached Relay query in the
// access-reviews tab would render edges pointing to a vanished record and
// crash with "Unexpected error :(".
func TestAccessReviewCampaign_DeleteRemovesFromListAndNode(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Source for Delete").
WithCsvData(testCsvData).
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Campaign to Cascade Delete").
WithAccessReviewSourceIDs([]string{sourceID}).
Create()
const deleteMutation = `
mutation($input: DeleteAccessReviewCampaignInput!) {
deleteAccessReviewCampaign(input: $input) {
deletedAccessReviewCampaignId
}
}
`
var deleteResult struct {
DeleteAccessReviewCampaign struct {
DeletedAccessReviewCampaignID string `json:"deletedAccessReviewCampaignId"`
} `json:"deleteAccessReviewCampaign"`
}
err := owner.Execute(deleteMutation, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, &deleteResult)
require.NoError(t, err)
assert.Equal(t, campaignID, deleteResult.DeleteAccessReviewCampaign.DeletedAccessReviewCampaignID)
// The campaign must no longer appear in the organization's campaign
// connection -- mirrors the AccessReviewCampaignsTabQuery the FE fires
// when the user navigates back to the access-reviews tab.
const listQuery = `
query($id: ID!) {
node(id: $id) {
... on Organization {
accessReviewCampaigns(first: 50) {
edges {
node { id }
}
}
}
}
}
`
var listResult struct {
Node struct {
AccessReviewCampaigns struct {
Edges []struct {
Node struct {
ID string `json:"id"`
} `json:"node"`
} `json:"edges"`
} `json:"accessReviewCampaigns"`
} `json:"node"`
}
err = owner.Execute(listQuery, map[string]any{"id": orgID}, &listResult)
require.NoError(t, err)
for _, edge := range listResult.Node.AccessReviewCampaigns.Edges {
assert.NotEqual(t, campaignID, edge.Node.ID, "deleted campaign must not appear in the connection")
}
// Resolving the deleted GID via `node(id:)` must error with NOT_FOUND so
// the cached Relay store can't keep serving a tombstoned record.
const nodeQuery = `
query($id: ID!) {
node(id: $id) {
... on AccessReviewCampaign {
id
}
}
}
`
_, err = owner.Do(nodeQuery, map[string]any{"id": campaignID})
var gqlErrors testutil.GraphQLErrors
require.ErrorAs(t, err, &gqlErrors)
require.Len(t, gqlErrors, 1)
assert.Equal(t, "NOT_FOUND", gqlErrors[0].Code())
}
func TestAccessReviewCampaign_List(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
for _, name := range []string{"Q1 Review", "Q2 Review", "Q3 Review"} {
factory.NewAccessReviewCampaign(owner, orgID).WithName(name).Create()
}
const query = `
query($id: ID!) {
node(id: $id) {
... on Organization {
accessReviewCampaigns(first: 10) {
edges {
node {
id
name
status
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
AccessReviewCampaigns struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"accessReviewCampaigns"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{"id": orgID}, &result)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AccessReviewCampaigns.TotalCount, 3)
for _, edge := range result.Node.AccessReviewCampaigns.Edges {
assert.Equal(t, "DRAFT", edge.Node.Status)
}
}
func TestAccessReviewCampaign_NodeQuery(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Node Query Campaign").
Create()
const query = `
query($id: ID!) {
node(id: $id) {
... on AccessReviewCampaign {
id
name
status
organization {
id
}
statistics {
totalCount
}
createdAt
updatedAt
}
}
}
`
var result struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Organization struct {
ID string `json:"id"`
} `json:"organization"`
Statistics struct {
TotalCount int `json:"totalCount"`
} `json:"statistics"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{"id": campaignID}, &result)
require.NoError(t, err)
assert.Equal(t, campaignID, result.Node.ID)
assert.Equal(t, "Node Query Campaign", result.Node.Name)
assert.Equal(t, "DRAFT", result.Node.Status)
assert.Equal(t, orgID, result.Node.Organization.ID)
assert.Equal(t, 0, result.Node.Statistics.TotalCount)
}
func TestAccessReviewCampaign_StartWithCsvSource(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("CSV Test Source").
WithCsvData(testCsvData).
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("CSV Campaign").
WithAccessReviewSourceIDs([]string{sourceID}).
Create()
const query = `
mutation($input: StartAccessReviewCampaignInput!) {
startAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
status
startedAt
}
}
}
`
var result struct {
StartAccessReviewCampaign struct {
AccessReviewCampaign struct {
ID string `json:"id"`
Status string `json:"status"`
StartedAt *string `json:"startedAt"`
} `json:"accessReviewCampaign"`
} `json:"startAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, &result)
require.NoError(t, err)
campaign := result.StartAccessReviewCampaign.AccessReviewCampaign
assert.Equal(t, campaignID, campaign.ID)
assert.Equal(t, "IN_PROGRESS", campaign.Status)
assert.NotNil(t, campaign.StartedAt)
}
func TestAccessReviewCampaign_AddAndRemoveCampaignSource(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Scope Source").
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Scope Management Campaign").
Create()
t.Run("add scope source", func(t *testing.T) {
const query = `
mutation($input: AddAccessReviewCampaignSourceInput!) {
addAccessReviewCampaignSource(input: $input) {
accessReviewCampaign {
id
sources {
id
name
source {
id
}
}
}
}
}
`
var result struct {
AddAccessReviewCampaignSource struct {
AccessReviewCampaign struct {
ID string `json:"id"`
CampaignSources []struct {
ID string `json:"id"`
Name string `json:"name"`
Source *struct {
ID string `json:"id"`
} `json:"source"`
} `json:"sources"`
} `json:"accessReviewCampaign"`
} `json:"addAccessReviewCampaignSource"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
"accessReviewSourceId": sourceID,
},
}, &result)
require.NoError(t, err)
campaign := result.AddAccessReviewCampaignSource.AccessReviewCampaign
assert.Equal(t, campaignID, campaign.ID)
assert.Len(t, campaign.CampaignSources, 1)
// The scope source id is now a per-campaign snapshot id, distinct from
// the live source id, which is exposed via the source link.
assert.NotEqual(t, sourceID, campaign.CampaignSources[0].ID)
require.NotNil(t, campaign.CampaignSources[0].Source)
assert.Equal(t, sourceID, campaign.CampaignSources[0].Source.ID)
})
t.Run("remove scope source", func(t *testing.T) {
const query = `
mutation($input: RemoveAccessReviewCampaignSourceInput!) {
removeAccessReviewCampaignSource(input: $input) {
accessReviewCampaign {
id
sources {
id
}
}
}
}
`
var result struct {
RemoveAccessReviewCampaignSource struct {
AccessReviewCampaign struct {
ID string `json:"id"`
CampaignSources []struct {
ID string `json:"id"`
} `json:"sources"`
} `json:"accessReviewCampaign"`
} `json:"removeAccessReviewCampaignSource"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
"accessReviewSourceId": sourceID,
},
}, &result)
require.NoError(t, err)
campaign := result.RemoveAccessReviewCampaignSource.AccessReviewCampaign
assert.Equal(t, campaignID, campaign.ID)
assert.Empty(t, campaign.CampaignSources)
})
}
func TestAccessReviewCampaignSource_Node(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Node Query Source").
WithCsvData(testCsvData).
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Node Query Campaign").
WithAccessReviewSourceIDs([]string{sourceID}).
Create()
const campaignQuery = `
query($id: ID!) {
node(id: $id) {
... on AccessReviewCampaign {
sources {
id
name
}
}
}
}
`
var campaignResult struct {
Node struct {
Sources []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"sources"`
} `json:"node"`
}
err := owner.Execute(campaignQuery, map[string]any{"id": campaignID}, &campaignResult)
require.NoError(t, err)
require.Len(t, campaignResult.Node.Sources, 1)
campaignSourceID := campaignResult.Node.Sources[0].ID
const startQuery = `
mutation($input: StartAccessReviewCampaignInput!) {
startAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
status
}
}
}
`
err = owner.Execute(startQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, nil)
require.NoError(t, err)
const nodeQuery = `
query($id: ID!) {
node(id: $id) {
__typename
... on AccessReviewCampaignSource {
id
campaign {
id
}
name
entries(first: 10) {
totalCount
}
}
}
}
`
var nodeResult struct {
Node struct {
Typename string `json:"__typename"`
ID string `json:"id"`
Campaign struct {
ID string `json:"id"`
} `json:"campaign"`
Name string `json:"name"`
Entries struct {
TotalCount int `json:"totalCount"`
} `json:"entries"`
} `json:"node"`
}
require.Eventually(t, func() bool {
err := owner.Execute(nodeQuery, map[string]any{"id": campaignSourceID}, &nodeResult)
if err != nil {
return false
}
return nodeResult.Node.Entries.TotalCount > 0
}, 60*time.Second, 1*time.Second, "campaign source should have entries after start")
assert.Equal(t, "AccessReviewCampaignSource", nodeResult.Node.Typename)
assert.Equal(t, campaignSourceID, nodeResult.Node.ID)
assert.Equal(t, campaignID, nodeResult.Node.Campaign.ID)
assert.Equal(t, "Node Query Source", nodeResult.Node.Name)
assert.Greater(t, nodeResult.Node.Entries.TotalCount, 0)
}
func TestAccessReviewCampaignSource_NameSurvivesSourceDeletion(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
const snapshotName = "Archived Snapshot Source"
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName(snapshotName).
WithCsvData(testCsvData).
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Archival Campaign").
WithAccessReviewSourceIDs([]string{sourceID}).
Create()
const sourcesQuery = `
query($id: ID!) {
node(id: $id) {
... on AccessReviewCampaign {
sources {
id
name
sourceId
source {
id
}
}
}
}
}
`
var before struct {
Node struct {
Sources []struct {
ID string `json:"id"`
Name string `json:"name"`
SourceID *string `json:"sourceId"`
Source *struct {
ID string `json:"id"`
} `json:"source"`
} `json:"sources"`
} `json:"node"`
}
err := owner.Execute(sourcesQuery, map[string]any{"id": campaignID}, &before)
require.NoError(t, err)
require.Len(t, before.Node.Sources, 1)
assert.Equal(t, snapshotName, before.Node.Sources[0].Name)
require.NotNil(t, before.Node.Sources[0].SourceID)
assert.Equal(t, sourceID, *before.Node.Sources[0].SourceID)
require.NotNil(t, before.Node.Sources[0].Source)
assert.Equal(t, sourceID, before.Node.Sources[0].Source.ID)
const deleteQuery = `
mutation($input: DeleteAccessReviewSourceInput!) {
deleteAccessReviewSource(input: $input) {
deletedAccessReviewSourceId
}
}
`
err = owner.Execute(deleteQuery, map[string]any{
"input": map[string]any{
"accessReviewSourceId": sourceID,
},
}, nil)
require.NoError(t, err)
var after struct {
Node struct {
Sources []struct {
ID string `json:"id"`
Name string `json:"name"`
SourceID *string `json:"sourceId"`
Source *struct {
ID string `json:"id"`
} `json:"source"`
} `json:"sources"`
} `json:"node"`
}
err = owner.Execute(sourcesQuery, map[string]any{"id": campaignID}, &after)
require.NoError(t, err)
require.Len(t, after.Node.Sources, 1)
assert.Equal(t, before.Node.Sources[0].ID, after.Node.Sources[0].ID)
assert.Equal(t, snapshotName, after.Node.Sources[0].Name)
assert.Nil(t, after.Node.Sources[0].SourceID)
assert.Nil(t, after.Node.Sources[0].Source)
}
func TestAccessReviewCampaign_Cancel(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Cancel Test Source").
WithCsvData(testCsvData).
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Campaign to Cancel").
WithAccessReviewSourceIDs([]string{sourceID}).
Create()
// Start the campaign first
const startQuery = `
mutation($input: StartAccessReviewCampaignInput!) {
startAccessReviewCampaign(input: $input) {
accessReviewCampaign { id status }
}
}
`
err := owner.Execute(startQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, nil)
require.NoError(t, err)
// Cancel it
const cancelQuery = `
mutation($input: CancelAccessReviewCampaignInput!) {
cancelAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
status
}
}
}
`
var result struct {
CancelAccessReviewCampaign struct {
AccessReviewCampaign struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"accessReviewCampaign"`
} `json:"cancelAccessReviewCampaign"`
}
err = owner.Execute(cancelQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, &result)
require.NoError(t, err)
assert.Equal(t, campaignID, result.CancelAccessReviewCampaign.AccessReviewCampaign.ID)
assert.Equal(t, "CANCELLED", result.CancelAccessReviewCampaign.AccessReviewCampaign.Status)
}
func TestAccessReviewCampaign_Description(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
t.Run("create with description", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewCampaignInput!) {
createAccessReviewCampaign(input: $input) {
accessReviewCampaignEdge {
node {
id
name
description
}
}
}
}
`
var result struct {
CreateAccessReviewCampaign struct {
AccessReviewCampaignEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
} `json:"node"`
} `json:"accessReviewCampaignEdge"`
} `json:"createAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "Q1 Review with Desc",
"description": "Quarterly review of all SaaS access",
},
}, &result)
require.NoError(t, err)
node := result.CreateAccessReviewCampaign.AccessReviewCampaignEdge.Node
assert.Equal(t, "Q1 Review with Desc", node.Name)
assert.Equal(t, "Quarterly review of all SaaS access", node.Description)
})
t.Run("update description", func(t *testing.T) {
t.Parallel()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Description Update Test").
Create()
const query = `
mutation($input: UpdateAccessReviewCampaignInput!) {
updateAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
description
}
}
}
`
var result struct {
UpdateAccessReviewCampaign struct {
AccessReviewCampaign struct {
ID string `json:"id"`
Description string `json:"description"`
} `json:"accessReviewCampaign"`
} `json:"updateAccessReviewCampaign"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
"description": "Updated description",
},
}, &result)
require.NoError(t, err)
assert.Equal(t, campaignID, result.UpdateAccessReviewCampaign.AccessReviewCampaign.ID)
assert.Equal(t, "Updated description", result.UpdateAccessReviewCampaign.AccessReviewCampaign.Description)
})
}
func TestAccessReviewCampaign_FullLifecycle(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
// Step 1: Create a CSV source with test data
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Lifecycle Test Source").
WithCsvData(testCsvData).
Create()
// Step 2: Create a campaign with a description and the source
const createQuery = `
mutation($input: CreateAccessReviewCampaignInput!) {
createAccessReviewCampaign(input: $input) {
accessReviewCampaignEdge {
node {
id
name
description
status
sources {
id
}
}
}
}
}
`
var createResult struct {
CreateAccessReviewCampaign struct {
AccessReviewCampaignEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
CampaignSources []struct {
ID string `json:"id"`
} `json:"sources"`
} `json:"node"`
} `json:"accessReviewCampaignEdge"`
} `json:"createAccessReviewCampaign"`
}
err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{
"organizationId": orgID,
"name": "Full Lifecycle Campaign",
"description": "Testing the full lifecycle",
"accessReviewSourceIds": []string{sourceID},
},
}, &createResult)
require.NoError(t, err)
campaignNode := createResult.CreateAccessReviewCampaign.AccessReviewCampaignEdge.Node
campaignID := campaignNode.ID
assert.Equal(t, "DRAFT", campaignNode.Status)
assert.Equal(t, "Testing the full lifecycle", campaignNode.Description)
assert.Len(t, campaignNode.CampaignSources, 1)
// Step 3: Start the campaign (triggers worker to fetch CSV data)
const startQuery = `
mutation($input: StartAccessReviewCampaignInput!) {
startAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
status
startedAt
}
}
}
`
var startResult struct {
StartAccessReviewCampaign struct {
AccessReviewCampaign struct {
ID string `json:"id"`
Status string `json:"status"`
StartedAt *string `json:"startedAt"`
} `json:"accessReviewCampaign"`
} `json:"startAccessReviewCampaign"`
}
err = owner.Execute(startQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, &startResult)
require.NoError(t, err)
assert.Equal(t, "IN_PROGRESS", startResult.StartAccessReviewCampaign.AccessReviewCampaign.Status)
assert.NotNil(t, startResult.StartAccessReviewCampaign.AccessReviewCampaign.StartedAt)
// Step 4: Wait for the worker to process entries and move campaign to PENDING_ACTIONS.
// Poll the campaign status until it transitions.
const nodeQuery = `
query($id: ID!) {
node(id: $id) {
... on AccessReviewCampaign {
id
status
entries(first: 100) {
edges {
node {
id
email
fullName
active
decision
}
}
totalCount
}
statistics {
totalCount
decisionCounts {
decision
count
}
}
}
}
}
`
type campaignQueryResult struct {
Node struct {
ID string `json:"id"`
Status string `json:"status"`
Entries struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Active *bool `json:"active"`
Decision string `json:"decision"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"entries"`
Statistics struct {
TotalCount int `json:"totalCount"`
DecisionCounts []struct {
Decision string `json:"decision"`
Count int `json:"count"`
} `json:"decisionCounts"`
} `json:"statistics"`
} `json:"node"`
}
var campaignResult campaignQueryResult
require.Eventually(t, func() bool {
err := owner.Execute(nodeQuery, map[string]any{"id": campaignID}, &campaignResult)
if err != nil {
return false
}
return campaignResult.Node.Status == "PENDING_ACTIONS"
}, 60*time.Second, 1*time.Second, "campaign should transition to PENDING_ACTIONS")
// Verify entries were created from CSV data
assert.GreaterOrEqual(t, campaignResult.Node.Entries.TotalCount, 1)
assert.Equal(t, campaignResult.Node.Entries.TotalCount, campaignResult.Node.Statistics.TotalCount)
// All entries should be PENDING
for _, edge := range campaignResult.Node.Entries.Edges {
assert.Equal(t, "PENDING", edge.Node.Decision)
if edge.Node.Email == "jane@example.com" {
require.NotNil(t, edge.Node.Active)
assert.True(t, *edge.Node.Active)
}
}
// Step 5: Record decisions on all entries
const recordDecisionQuery = `
mutation($input: RecordAccessReviewEntryDecisionInput!) {
recordAccessReviewEntryDecision(input: $input) {
accessReviewEntry {
id
decision
decidedAt
decisionHistory {
id
decision
decidedAt
}
}
}
}
`
for _, edge := range campaignResult.Node.Entries.Edges {
var decisionResult struct {
RecordAccessReviewEntryDecision struct {
AccessReviewEntry struct {
ID string `json:"id"`
Decision string `json:"decision"`
DecidedAt *string `json:"decidedAt"`
DecisionHistory []struct {
ID string `json:"id"`
Decision string `json:"decision"`
} `json:"decisionHistory"`
} `json:"accessReviewEntry"`
} `json:"recordAccessReviewEntryDecision"`
}
err = owner.Execute(recordDecisionQuery, map[string]any{
"input": map[string]any{
"accessReviewEntryId": edge.Node.ID,
"decision": "APPROVED",
},
}, &decisionResult)
require.NoError(t, err)
entry := decisionResult.RecordAccessReviewEntryDecision.AccessReviewEntry
assert.Equal(t, "APPROVED", entry.Decision)
assert.NotNil(t, entry.DecidedAt)
// Verify decision history was recorded
assert.Len(t, entry.DecisionHistory, 1)
assert.Equal(t, "APPROVED", entry.DecisionHistory[0].Decision)
}
// Step 6: Close the campaign
const closeQuery = `
mutation($input: CloseAccessReviewCampaignInput!) {
closeAccessReviewCampaign(input: $input) {
accessReviewCampaign {
id
status
completedAt
}
}
}
`
var closeResult struct {
CloseAccessReviewCampaign struct {
AccessReviewCampaign struct {
ID string `json:"id"`
Status string `json:"status"`
CompletedAt *string `json:"completedAt"`
} `json:"accessReviewCampaign"`
} `json:"closeAccessReviewCampaign"`
}
err = owner.Execute(closeQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, &closeResult)
require.NoError(t, err)
closedCampaign := closeResult.CloseAccessReviewCampaign.AccessReviewCampaign
assert.Equal(t, "COMPLETED", closedCampaign.Status)
assert.NotNil(t, closedCampaign.CompletedAt)
}
func TestAccessReviewCampaign_CloseRequiresAllDecisions(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
sourceID := factory.NewAccessReviewSource(owner, orgID).
WithName("Close Guard Source").
WithCsvData(testCsvData).
Create()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("Close Guard Campaign").
WithAccessReviewSourceIDs([]string{sourceID}).
Create()
// Start the campaign
const startQuery = `
mutation($input: StartAccessReviewCampaignInput!) {
startAccessReviewCampaign(input: $input) {
accessReviewCampaign { id status }
}
}
`
err := owner.Execute(startQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
}, nil)
require.NoError(t, err)
// Wait for PENDING_ACTIONS
const nodeQuery = `
query($id: ID!) {
node(id: $id) {
... on AccessReviewCampaign { status }
}
}
`
require.Eventually(t, func() bool {
var r struct {
Node struct {
Status string `json:"status"`
} `json:"node"`
}
if err := owner.Execute(nodeQuery, map[string]any{"id": campaignID}, &r); err != nil {
return false
}
return r.Node.Status == "PENDING_ACTIONS"
}, 60*time.Second, 1*time.Second)
// Try to close without deciding — should fail
const closeQuery = `
mutation($input: CloseAccessReviewCampaignInput!) {
closeAccessReviewCampaign(input: $input) {
accessReviewCampaign { id status }
}
}
`
_, err = owner.Do(closeQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
})
require.Error(t, err, "closing a campaign with undecided entries should fail")
}
func TestAccessReviewCampaign_StartWithoutSourcesFails(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String()
campaignID := factory.NewAccessReviewCampaign(owner, orgID).
WithName("No Sources Campaign").
Create()
const startQuery = `
mutation($input: StartAccessReviewCampaignInput!) {
startAccessReviewCampaign(input: $input) {
accessReviewCampaign { id status }
}
}
`
_, err := owner.Do(startQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
},
})
require.Error(t, err, "starting a campaign without sources should fail")
}
func TestAccessReview_TenantIsolation(t *testing.T) {
t.Parallel()
org1Owner := testutil.NewClient(t, testutil.RoleOwner)
org2Owner := testutil.NewClient(t, testutil.RoleOwner)
org1ID := org1Owner.GetOrganizationID().String()
t.Run("cannot create access source in another organization", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewSourceInput!) {
createAccessReviewSource(input: $input) {
accessReviewSourceEdge {
node { id }
}
}
}
`
_, err := org2Owner.Do(query, map[string]any{
"input": map[string]any{
"organizationId": org1ID,
"name": "Unauthorized Source",
},
})
require.Error(t, err, "Should not be able to create access source in another organization")
})
t.Run("cannot create campaign in another organization", func(t *testing.T) {
t.Parallel()
const query = `
mutation($input: CreateAccessReviewCampaignInput!) {
createAccessReviewCampaign(input: $input) {
accessReviewCampaignEdge {
node { id }
}
}
}
`
_, err := org2Owner.Do(query, map[string]any{
"input": map[string]any{
"organizationId": org1ID,
"name": "Unauthorized Campaign",
},
})
require.Error(t, err, "Should not be able to create campaign in another organization")
})
}