Files
probo/e2e/console/access_review_test.go
Sacha Al Himdani 4c57d201a4 Make license declarations consistently MIT
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:

- Convert every source-file header to the MIT text across all comment
  styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
  SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
  "MIT License" title line
- Switch the package.json license fields, Docker image label, and
  cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
  (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
  hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
  the comma-separated years to a hyphenated range

Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-13 16:21:14 +02:00

1550 lines
40 KiB
Go

// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// 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)
})
}
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")
})
}