From 70881d0f5a619366a144bdea21923c4d1c8d3e91 Mon Sep 17 00:00:00 2001 From: Dmitrii Andreev Date: Thu, 23 Apr 2026 11:52:36 -0500 Subject: [PATCH 1/2] HYPERFLEET-971 - feat: reject nodepool create/patch on soft-deleted cluster --- pkg/errors/errors.go | 9 + pkg/handlers/cluster_nodepools.go | 14 +- pkg/handlers/cluster_nodepools_test.go | 334 ++++++++++++++++++++++++- 3 files changed, 349 insertions(+), 8 deletions(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index d9284665..e4de9183 100755 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -52,6 +52,7 @@ const ( // Conflict errors (CNF) - 409 CodeConflictExists = "HYPERFLEET-CNF-001" CodeConflictVersion = "HYPERFLEET-CNF-002" + CodeConflictState = "HYPERFLEET-CNF-003" // Rate Limit errors (LMT) - 429 CodeRateLimitExceeded = "HYPERFLEET-LMT-001" @@ -166,6 +167,10 @@ var errorDefinitions = map[string]errorDefinition{ CodeConflictVersion: { ErrorTypeConflict, "Version Conflict", "The resource version does not match", http.StatusConflict, }, + CodeConflictState: { + ErrorTypeConflict, "State Conflict", + "Operation not allowed in current resource state", http.StatusConflict, + }, // Rate Limit errors (LMT) - 429 CodeRateLimitExceeded: { @@ -363,6 +368,10 @@ func Conflict(reason string, values ...interface{}) *ServiceError { return New(CodeConflictExists, reason, values...) } +func ConflictState(reason string, values ...interface{}) *ServiceError { + return New(CodeConflictState, reason, values...) +} + func Validation(reason string, values ...interface{}) *ServiceError { return New(CodeValidationMultiple, reason, values...) } diff --git a/pkg/handlers/cluster_nodepools.go b/pkg/handlers/cluster_nodepools.go index 00b4789d..5fff71f3 100644 --- a/pkg/handlers/cluster_nodepools.go +++ b/pkg/handlers/cluster_nodepools.go @@ -190,11 +190,15 @@ func (h ClusterNodePoolsHandler) Patch(w http.ResponseWriter, r *http.Request) { clusterID := mux.Vars(r)["id"] nodePoolID := mux.Vars(r)["nodepool_id"] - _, err := h.clusterService.Get(ctx, clusterID) + cluster, err := h.clusterService.Get(ctx, clusterID) if err != nil { return nil, err } + if cluster.DeletedTime != nil { + return nil, errors.ConflictState("Cluster '%s' is marked for deletion", clusterID) + } + found, err := h.nodePoolService.Get(ctx, nodePoolID) if err != nil { return nil, err @@ -204,6 +208,10 @@ func (h ClusterNodePoolsHandler) Patch(w http.ResponseWriter, r *http.Request) { return nil, errors.NotFound("NodePool '%s' not found for cluster '%s'", nodePoolID, clusterID) } + if found.DeletedTime != nil { + return nil, errors.ConflictState("NodePool '%s' is marked for deletion", nodePoolID) + } + if patch.Spec != nil { specJSON, jsonErr := json.Marshal(*patch.Spec) if jsonErr != nil { @@ -258,6 +266,10 @@ func (h ClusterNodePoolsHandler) Create(w http.ResponseWriter, r *http.Request) return nil, err } + if cluster.DeletedTime != nil { + return nil, errors.ConflictState("Cluster '%s' is marked for deletion", clusterID) + } + // Use the presenters.ConvertNodePool helper to convert the request nodePoolModel, convErr := presenters.ConvertNodePool(&req, cluster.ID, "system@hyperfleet.local") if convErr != nil { diff --git a/pkg/handlers/cluster_nodepools_test.go b/pkg/handlers/cluster_nodepools_test.go index 9e8666c4..6b7e1ac2 100644 --- a/pkg/handlers/cluster_nodepools_test.go +++ b/pkg/handlers/cluster_nodepools_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -17,12 +18,18 @@ import ( "github.com/openshift-hyperfleet/hyperfleet-api/pkg/services" ) +const ( + testClusterID = "test-cluster-id" + testNodePoolID = "test-nodepool-id" + testSystemUser = "system@hyperfleet.local" +) + func TestClusterNodePoolsHandler_Get(t *testing.T) { RegisterTestingT(t) now := time.Now() - clusterID := "test-cluster-123" - nodePoolID := "test-nodepool-456" + clusterID := testClusterID + nodePoolID := testNodePoolID tests := []struct { setupMocks func(ctrl *gomock.Controller) ( //nolint:lll @@ -206,9 +213,8 @@ func TestClusterNodePoolsHandler_SoftDelete(t *testing.T) { RegisterTestingT(t) now := time.Now() - deletedBy := "system@hyperfleet.local" - clusterID := "test-cluster-123" - nodePoolID := "test-nodepool-456" + clusterID := testClusterID + nodePoolID := testNodePoolID tests := []struct { setupMocks func(ctrl *gomock.Controller) ( //nolint:lll @@ -242,13 +248,14 @@ func TestClusterNodePoolsHandler_SoftDelete(t *testing.T) { }, nil) deletedTime := now + deletedByUser := testSystemUser mockNodePoolSvc.EXPECT().SoftDelete(gomock.Any(), nodePoolID).Return(&api.NodePool{ Meta: api.Meta{ID: nodePoolID, CreatedTime: now, UpdatedTime: now}, Kind: "NodePool", Name: "test-nodepool", OwnerID: clusterID, DeletedTime: &deletedTime, - DeletedBy: &deletedBy, + DeletedBy: &deletedByUser, Spec: []byte("{}"), Labels: []byte("{}"), StatusConditions: []byte("[]"), @@ -359,7 +366,320 @@ func TestClusterNodePoolsHandler_SoftDelete(t *testing.T) { Expect(err).NotTo(HaveOccurred()) Expect(*response.Id).To(Equal(nodePoolID)) Expect(response.DeletedTime).NotTo(BeNil()) - Expect(string(*response.DeletedBy)).To(Equal("system@hyperfleet.local")) + Expect(string(*response.DeletedBy)).To(Equal(testSystemUser)) + } + }) + } +} + +func TestClusterNodePoolsHandler_Create(t *testing.T) { + RegisterTestingT(t) + + now := time.Now() + clusterID := testClusterID + nodePoolID := testNodePoolID + validBody := `{"name":"test-np","kind":"NodePool","spec":{"replicas":1}}` + + tests := []struct { + setupMocks func(ctrl *gomock.Controller) ( //nolint:lll + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) + name string + clusterID string + expectedStatusCode int + expectedError bool + }{ + { + name: "Success - Create nodepool for active cluster", + clusterID: clusterID, + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + }, nil) + + mockNodePoolSvc.EXPECT().Create(gomock.Any(), gomock.Any()).Return(&api.NodePool{ + Meta: api.Meta{ID: nodePoolID, CreatedTime: now, UpdatedTime: now}, + Kind: "NodePool", + Name: "test-np", + OwnerID: clusterID, + Spec: []byte(`{"replicas":1}`), + Labels: []byte("{}"), + StatusConditions: []byte("[]"), + CreatedBy: testSystemUser, + UpdatedBy: testSystemUser, + }, nil) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusCreated, + expectedError: false, + }, + { + name: "Error 409 - Cluster is soft-deleted", + clusterID: clusterID, + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + deletedTime := now + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + DeletedTime: &deletedTime, + }, nil) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusConflict, + expectedError: true, + }, + { + name: "Error 404 - Cluster not found", + clusterID: "non-existent", + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), "non-existent").Return(nil, errors.NotFound("Cluster not found")) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusNotFound, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterTestingT(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClusterSvc, mockNodePoolSvc, mockGenericSvc := tt.setupMocks(ctrl) + handler := NewClusterNodePoolsHandler(mockClusterSvc, mockNodePoolSvc, mockGenericSvc) + + reqURL := "/api/hyperfleet/v1/clusters/" + tt.clusterID + "/nodepools" + req := httptest.NewRequest(http.MethodPost, reqURL, strings.NewReader(validBody)) + req.Header.Set("Content-Type", "application/json") + req = mux.SetURLVars(req, map[string]string{ + "id": tt.clusterID, + }) + + rr := httptest.NewRecorder() + handler.Create(rr, req) + + Expect(rr.Code).To(Equal(tt.expectedStatusCode)) + + if !tt.expectedError { + var response openapi.NodePool + err := json.Unmarshal(rr.Body.Bytes(), &response) + Expect(err).NotTo(HaveOccurred()) + Expect(*response.Id).To(Equal(nodePoolID)) + Expect(response.Kind).NotTo(BeNil()) + Expect(*response.Kind).To(Equal("NodePool")) + } + + if tt.expectedStatusCode == http.StatusConflict { + var errResp openapi.Error + err := json.Unmarshal(rr.Body.Bytes(), &errResp) + Expect(err).NotTo(HaveOccurred()) + Expect(errResp.Status).To(Equal(http.StatusConflict)) + Expect(*errResp.Detail).To(ContainSubstring("marked for deletion")) + Expect(*errResp.Code).To(Equal("HYPERFLEET-CNF-003")) + } + }) + } +} + +func TestClusterNodePoolsHandler_Patch(t *testing.T) { + RegisterTestingT(t) + + now := time.Now() + clusterID := testClusterID + nodePoolID := testNodePoolID + validBody := `{"spec":{"replicas":2}}` + + tests := []struct { + setupMocks func(ctrl *gomock.Controller) ( //nolint:lll + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) + name string + clusterID string + nodePoolID string + expectedStatusCode int + expectedError bool + }{ + { + name: "Success - Patch nodepool for active cluster", + clusterID: clusterID, + nodePoolID: nodePoolID, + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + }, nil) + + mockNodePoolSvc.EXPECT().Get(gomock.Any(), nodePoolID).Return(&api.NodePool{ + Meta: api.Meta{ID: nodePoolID, CreatedTime: now, UpdatedTime: now}, + Kind: "NodePool", + Name: "test-nodepool", + OwnerID: clusterID, + Spec: []byte("{}"), + Labels: []byte("{}"), + StatusConditions: []byte("[]"), + CreatedBy: "user@example.com", + UpdatedBy: "user@example.com", + }, nil) + + mockNodePoolSvc.EXPECT().Replace(gomock.Any(), gomock.Any()).Return(&api.NodePool{ + Meta: api.Meta{ID: nodePoolID, CreatedTime: now, UpdatedTime: now}, + Kind: "NodePool", + Name: "test-nodepool", + OwnerID: clusterID, + Spec: []byte(`{"replicas":2}`), + Labels: []byte("{}"), + StatusConditions: []byte("[]"), + CreatedBy: "user@example.com", + UpdatedBy: "user@example.com", + }, nil) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusOK, + expectedError: false, + }, + { + name: "Error 409 - Cluster is soft-deleted", + clusterID: clusterID, + nodePoolID: nodePoolID, + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + deletedTime := now + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + DeletedTime: &deletedTime, + }, nil) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusConflict, + expectedError: true, + }, + { + name: "Error 409 - NodePool is soft-deleted", + clusterID: clusterID, + nodePoolID: nodePoolID, + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + }, nil) + + deletedTime := now + mockNodePoolSvc.EXPECT().Get(gomock.Any(), nodePoolID).Return(&api.NodePool{ + Meta: api.Meta{ID: nodePoolID, CreatedTime: now, UpdatedTime: now}, + Kind: "NodePool", + Name: "test-nodepool", + OwnerID: clusterID, + DeletedTime: &deletedTime, + }, nil) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusConflict, + expectedError: true, + }, + { + name: "Error 404 - Cluster not found", + clusterID: "non-existent", + nodePoolID: nodePoolID, + setupMocks: func(ctrl *gomock.Controller) ( + *services.MockClusterService, *services.MockNodePoolService, *services.MockGenericService, + ) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockNodePoolSvc := services.NewMockNodePoolService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), "non-existent").Return(nil, errors.NotFound("Cluster not found")) + + return mockClusterSvc, mockNodePoolSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusNotFound, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterTestingT(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClusterSvc, mockNodePoolSvc, mockGenericSvc := tt.setupMocks(ctrl) + handler := NewClusterNodePoolsHandler(mockClusterSvc, mockNodePoolSvc, mockGenericSvc) + + reqURL := "/api/hyperfleet/v1/clusters/" + tt.clusterID + "/nodepools/" + tt.nodePoolID + req := httptest.NewRequest(http.MethodPatch, reqURL, strings.NewReader(validBody)) + req.Header.Set("Content-Type", "application/json") + req = mux.SetURLVars(req, map[string]string{ + "id": tt.clusterID, + "nodepool_id": tt.nodePoolID, + }) + + rr := httptest.NewRecorder() + handler.Patch(rr, req) + + Expect(rr.Code).To(Equal(tt.expectedStatusCode)) + + if !tt.expectedError { + var response openapi.NodePool + err := json.Unmarshal(rr.Body.Bytes(), &response) + Expect(err).NotTo(HaveOccurred()) + Expect(*response.Id).To(Equal(nodePoolID)) + Expect(response.Kind).NotTo(BeNil()) + Expect(*response.Kind).To(Equal("NodePool")) + } + + if tt.expectedStatusCode == http.StatusConflict { + var errResp openapi.Error + err := json.Unmarshal(rr.Body.Bytes(), &errResp) + Expect(err).NotTo(HaveOccurred()) + Expect(errResp.Status).To(Equal(http.StatusConflict)) + Expect(*errResp.Detail).To(ContainSubstring("marked for deletion")) + Expect(*errResp.Code).To(Equal("HYPERFLEET-CNF-003")) } }) } From 1c2efd3c37c3f19fbc9e376aa9ee04e5abd4de9d Mon Sep 17 00:00:00 2001 From: Dmitrii Andreev Date: Thu, 23 Apr 2026 11:58:17 -0500 Subject: [PATCH 2/2] HYPERFLEET-971 - feat: reject cluster patch on soft-deleted cluster --- pkg/handlers/cluster.go | 4 + pkg/handlers/cluster_test.go | 141 +++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 pkg/handlers/cluster_test.go diff --git a/pkg/handlers/cluster.go b/pkg/handlers/cluster.go index f76683c5..82de137c 100644 --- a/pkg/handlers/cluster.go +++ b/pkg/handlers/cluster.go @@ -76,6 +76,10 @@ func (h ClusterHandler) Patch(w http.ResponseWriter, r *http.Request) { return nil, err } + if found.DeletedTime != nil { + return nil, errors.ConflictState("Cluster '%s' is marked for deletion", id) + } + if patch.Spec != nil { specJSON, err := json.Marshal(*patch.Spec) if err != nil { diff --git a/pkg/handlers/cluster_test.go b/pkg/handlers/cluster_test.go new file mode 100644 index 00000000..63709548 --- /dev/null +++ b/pkg/handlers/cluster_test.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gorilla/mux" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + + "github.com/openshift-hyperfleet/hyperfleet-api/pkg/api" + "github.com/openshift-hyperfleet/hyperfleet-api/pkg/api/openapi" + "github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors" + "github.com/openshift-hyperfleet/hyperfleet-api/pkg/services" +) + +func TestClusterHandler_Patch(t *testing.T) { + RegisterTestingT(t) + + now := time.Now() + clusterID := testClusterID + validBody := `{"spec":{"region":"us-east1"}}` + + tests := []struct { + setupMocks func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockGenericService) + name string + clusterID string + expectedStatusCode int + expectedError bool + }{ + { + name: "Success - Patch active cluster", + clusterID: clusterID, + setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockGenericService) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + Spec: []byte("{}"), + Labels: []byte("{}"), + StatusConditions: []byte("[]"), + CreatedBy: testSystemUser, + UpdatedBy: testSystemUser, + }, nil) + + mockClusterSvc.EXPECT().Replace(gomock.Any(), gomock.Any()).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + Spec: []byte(`{"region":"us-east1"}`), + Labels: []byte("{}"), + StatusConditions: []byte("[]"), + CreatedBy: testSystemUser, + UpdatedBy: testSystemUser, + }, nil) + + return mockClusterSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusOK, + expectedError: false, + }, + { + name: "Error 409 - Cluster is soft-deleted", + clusterID: clusterID, + setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockGenericService) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + deletedTime := now + mockClusterSvc.EXPECT().Get(gomock.Any(), clusterID).Return(&api.Cluster{ + Meta: api.Meta{ID: clusterID, CreatedTime: now, UpdatedTime: now}, + Name: "test-cluster", + DeletedTime: &deletedTime, + }, nil) + + return mockClusterSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusConflict, + expectedError: true, + }, + { + name: "Error 404 - Cluster not found", + clusterID: "non-existent", + setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockGenericService) { + mockClusterSvc := services.NewMockClusterService(ctrl) + mockGenericSvc := services.NewMockGenericService(ctrl) + + mockClusterSvc.EXPECT().Get(gomock.Any(), "non-existent").Return(nil, errors.NotFound("Cluster not found")) + + return mockClusterSvc, mockGenericSvc + }, + expectedStatusCode: http.StatusNotFound, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + RegisterTestingT(t) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockClusterSvc, mockGenericSvc := tt.setupMocks(ctrl) + handler := NewClusterHandler(mockClusterSvc, mockGenericSvc) + + reqURL := "/api/hyperfleet/v1/clusters/" + tt.clusterID + req := httptest.NewRequest(http.MethodPatch, reqURL, strings.NewReader(validBody)) + req.Header.Set("Content-Type", "application/json") + req = mux.SetURLVars(req, map[string]string{ + "id": tt.clusterID, + }) + + rr := httptest.NewRecorder() + handler.Patch(rr, req) + + Expect(rr.Code).To(Equal(tt.expectedStatusCode)) + + if !tt.expectedError { + var response openapi.Cluster + err := json.Unmarshal(rr.Body.Bytes(), &response) + Expect(err).NotTo(HaveOccurred()) + Expect(*response.Id).To(Equal(clusterID)) + } + + if tt.expectedStatusCode == http.StatusConflict { + var errResp openapi.Error + err := json.Unmarshal(rr.Body.Bytes(), &errResp) + Expect(err).NotTo(HaveOccurred()) + Expect(errResp.Status).To(Equal(http.StatusConflict)) + Expect(*errResp.Detail).To(ContainSubstring("marked for deletion")) + Expect(*errResp.Code).To(Equal("HYPERFLEET-CNF-003")) + } + }) + } +}