diff --git a/pkg/accessreview/campaign_service.go b/pkg/accessreview/campaign_service.go index 9e4740936..4e531634d 100644 --- a/pkg/accessreview/campaign_service.go +++ b/pkg/accessreview/campaign_service.go @@ -159,7 +159,7 @@ func (s *Service) UpdateCampaign( } if campaign.Status != coredata.AccessReviewCampaignStatusDraft { - return ErrCampaignCannotUpdate + return fmt.Errorf("cannot update campaign: %w", CampaignStatusError(campaign.Status)) } if req.Name != nil && *req.Name != nil { @@ -242,10 +242,8 @@ func (s *Service) AddCampaignSource( } if campaign.Status != coredata.AccessReviewCampaignStatusDraft { - return ErrCampaignCannotUpdate + return fmt.Errorf("cannot add scope source: %w", CampaignStatusError(campaign.Status)) } - - source := &coredata.AccessReviewSource{} if err := source.LoadByID(ctx, conn, scope, req.AccessReviewSourceID); err != nil { if errors.Is(err, coredata.ErrResourceNotFound) { return coredata.ErrResourceNotFound @@ -291,10 +289,8 @@ func (s *Service) RemoveCampaignSource( } if campaign.Status != coredata.AccessReviewCampaignStatusDraft { - return ErrCampaignCannotUpdate + return fmt.Errorf("cannot remove scope source: %w", CampaignStatusError(campaign.Status)) } - - campaignSource := &coredata.AccessReviewCampaignSource{} if err := campaignSource.DeleteByCampaignIDAndAccessReviewSourceID(ctx, conn, scope, campaign.ID, req.AccessReviewSourceID); err != nil { return fmt.Errorf("cannot delete campaign source: %w", err) } @@ -400,7 +396,7 @@ func (s *Service) StartCampaign( } if campaign.Status != coredata.AccessReviewCampaignStatusDraft { - return ErrCampaignCannotStart + return fmt.Errorf("cannot start campaign: %w", CampaignStatusError(campaign.Status)) } var campaignSources coredata.AccessReviewCampaignSources diff --git a/pkg/accessreview/errors.go b/pkg/accessreview/errors.go index c004ba860..d6259a090 100644 --- a/pkg/accessreview/errors.go +++ b/pkg/accessreview/errors.go @@ -20,10 +20,31 @@ package accessreview -import "errors" +import ( + "errors" + + "go.probo.inc/probo/pkg/coredata" +) var ( ErrCampaignMissingSources = errors.New("cannot start campaign: no scope sources configured") - ErrCampaignCannotStart = errors.New("cannot start campaign: only draft campaigns can be started") - ErrCampaignCannotUpdate = errors.New("cannot update campaign: only draft campaigns can be updated") + ErrCampaignInProgress = errors.New("campaign is in progress") + ErrCampaignPendingActions = errors.New("campaign is pending actions") + ErrCampaignCompleted = errors.New("campaign is completed") + ErrCampaignCancelled = errors.New("campaign is cancelled") ) + +func CampaignStatusError(status coredata.AccessReviewCampaignStatus) error { + switch status { + case coredata.AccessReviewCampaignStatusInProgress: + return ErrCampaignInProgress + case coredata.AccessReviewCampaignStatusPendingActions: + return ErrCampaignPendingActions + case coredata.AccessReviewCampaignStatusCompleted: + return ErrCampaignCompleted + case coredata.AccessReviewCampaignStatusCancelled: + return ErrCampaignCancelled + default: + return ErrCampaignInProgress + } +} diff --git a/pkg/accessreview/errors_test.go b/pkg/accessreview/errors_test.go new file mode 100644 index 000000000..8e81749eb --- /dev/null +++ b/pkg/accessreview/errors_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 accessreview + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "go.probo.inc/probo/pkg/coredata" +) + +func TestCampaignStatusError(t *testing.T) { + t.Parallel() + + tests := []struct { + status coredata.AccessReviewCampaignStatus + want error + }{ + {status: coredata.AccessReviewCampaignStatusInProgress, want: ErrCampaignInProgress}, + {status: coredata.AccessReviewCampaignStatusPendingActions, want: ErrCampaignPendingActions}, + {status: coredata.AccessReviewCampaignStatusCompleted, want: ErrCampaignCompleted}, + {status: coredata.AccessReviewCampaignStatusCancelled, want: ErrCampaignCancelled}, + } + + for _, tt := range tests { + t.Run(string(tt.status), func(t *testing.T) { + t.Parallel() + + err := fmt.Errorf("cannot start campaign: %w", CampaignStatusError(tt.status)) + assert.ErrorIs(t, err, tt.want) + }) + } +} diff --git a/pkg/server/api/console/v1/access_review_campaign_resolvers.go b/pkg/server/api/console/v1/access_review_campaign_resolvers.go index 76be1e5ba..4c04723cb 100644 --- a/pkg/server/api/console/v1/access_review_campaign_resolvers.go +++ b/pkg/server/api/console/v1/access_review_campaign_resolvers.go @@ -760,7 +760,10 @@ func (r *mutationResolver) UpdateAccessReviewCampaign(ctx context.Context, input return nil, gqlutils.NotFound(ctx, err) } - if errors.Is(err, accessreview.ErrCampaignCannotUpdate) { + if errors.Is(err, accessreview.ErrCampaignInProgress) || + errors.Is(err, accessreview.ErrCampaignPendingActions) || + errors.Is(err, accessreview.ErrCampaignCompleted) || + errors.Is(err, accessreview.ErrCampaignCancelled) { return nil, gqlutils.Invalid(ctx, err) } @@ -806,7 +809,10 @@ func (r *mutationResolver) StartAccessReviewCampaign(ctx context.Context, input campaign, err := r.accessReview.StartCampaign(ctx, scope, input.AccessReviewCampaignID) if err != nil { if errors.Is(err, accessreview.ErrCampaignMissingSources) || - errors.Is(err, accessreview.ErrCampaignCannotStart) { + errors.Is(err, accessreview.ErrCampaignInProgress) || + errors.Is(err, accessreview.ErrCampaignPendingActions) || + errors.Is(err, accessreview.ErrCampaignCompleted) || + errors.Is(err, accessreview.ErrCampaignCancelled) { return nil, gqlutils.Invalid(ctx, err) } @@ -878,6 +884,13 @@ func (r *mutationResolver) AddAccessReviewCampaignSource(ctx context.Context, in return nil, gqlutils.NotFound(ctx, err) } + if errors.Is(err, accessreview.ErrCampaignInProgress) || + errors.Is(err, accessreview.ErrCampaignPendingActions) || + errors.Is(err, accessreview.ErrCampaignCompleted) || + errors.Is(err, accessreview.ErrCampaignCancelled) { + return nil, gqlutils.Invalid(ctx, err) + } + r.logger.ErrorCtx(ctx, "cannot add scope source to access review campaign", log.Error(err)) return nil, gqlutils.Internal(ctx)