From ea22de7ced10b768b068379edeace2279fae6441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Mon, 11 May 2026 11:55:41 +0200 Subject: [PATCH] Cover access-review campaign delete with e2e test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock the backend contract the console relies on after deleting a campaign: the deleted GID must be gone from the organization's accessReviewCampaigns connection and node(id:) must return NOT_FOUND. The frontend caches both queries and would crash again if either contract slipped (e.g. a stale row, a wrong error code, or a missing cascade on scope sources). Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- e2e/console/access_review_test.go | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/e2e/console/access_review_test.go b/e2e/console/access_review_test.go index 3207e7265..23d95239c 100644 --- a/e2e/console/access_review_test.go +++ b/e2e/console/access_review_test.go @@ -466,6 +466,103 @@ func TestAccessReviewCampaign_Delete(t *testing.T) { 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.NewAccessSource(owner, orgID). + WithName("Source for Delete"). + WithCsvData(testCsvData). + Create() + campaignID := factory.NewAccessReviewCampaign(owner, orgID). + WithName("Campaign to Cascade Delete"). + WithAccessSourceIDs([]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)