Skip to content

Commit 29d2512

Browse files
Merge pull request #179 from pnguyen44/addNodepoolIntegrationTests
HYPERFLEET-1115- test: add noodpool integration tests
2 parents e05248b + e426478 commit 29d2512

3 files changed

Lines changed: 170 additions & 7 deletions

File tree

pkg/handlers/cluster_nodepools_test.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,39 @@ func TestClusterNodePoolsHandler_ForceDelete(t *testing.T) {
584584
},
585585
expectedStatusCode: http.StatusNoContent,
586586
},
587+
{
588+
name: "Error 400 - malformed JSON",
589+
nodePoolID: nodePoolID,
590+
body: `not json`,
591+
setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockNodePoolService) {
592+
mockClusterSvc := services.NewMockClusterService(ctrl)
593+
mockNodePoolSvc := services.NewMockNodePoolService(ctrl)
594+
return mockClusterSvc, mockNodePoolSvc
595+
},
596+
expectedStatusCode: http.StatusBadRequest,
597+
},
598+
{
599+
name: "Error 400 - empty reason",
600+
nodePoolID: nodePoolID,
601+
body: `{"reason": ""}`,
602+
setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockNodePoolService) {
603+
mockClusterSvc := services.NewMockClusterService(ctrl)
604+
mockNodePoolSvc := services.NewMockNodePoolService(ctrl)
605+
return mockClusterSvc, mockNodePoolSvc
606+
},
607+
expectedStatusCode: http.StatusBadRequest,
608+
},
609+
{
610+
name: "Error 400 - reason exceeds max length",
611+
nodePoolID: nodePoolID,
612+
body: `{"reason": "` + strings.Repeat("x", maxReasonLength+1) + `"}`,
613+
setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockNodePoolService) {
614+
mockClusterSvc := services.NewMockClusterService(ctrl)
615+
mockNodePoolSvc := services.NewMockNodePoolService(ctrl)
616+
return mockClusterSvc, mockNodePoolSvc
617+
},
618+
expectedStatusCode: http.StatusBadRequest,
619+
},
587620
{
588621
name: "Error 404 - nodepool not found",
589622
nodePoolID: "non-existent-id",
@@ -616,26 +649,35 @@ func TestClusterNodePoolsHandler_ForceDelete(t *testing.T) {
616649
expectedStatusCode: http.StatusConflict,
617650
},
618651
{
619-
name: "Error 400 - empty reason",
652+
name: "Error 500 - ownership lookup fails",
620653
nodePoolID: nodePoolID,
621-
body: `{"reason": ""}`,
654+
body: `{"reason": "some reason"}`,
622655
setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockNodePoolService) {
623656
mockClusterSvc := services.NewMockClusterService(ctrl)
624657
mockNodePoolSvc := services.NewMockNodePoolService(ctrl)
658+
mockNodePoolSvc.EXPECT().
659+
GetByIDAndOwner(gomock.Any(), nodePoolID, clusterID).
660+
Return(nil, errors.GeneralError("database connection lost"))
625661
return mockClusterSvc, mockNodePoolSvc
626662
},
627-
expectedStatusCode: http.StatusBadRequest,
663+
expectedStatusCode: http.StatusInternalServerError,
628664
},
629665
{
630-
name: "Error 400 - malformed JSON",
666+
name: "Error 500 - force-delete service error",
631667
nodePoolID: nodePoolID,
632-
body: `not json`,
668+
body: `{"reason": "some reason"}`,
633669
setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockNodePoolService) {
634670
mockClusterSvc := services.NewMockClusterService(ctrl)
635671
mockNodePoolSvc := services.NewMockNodePoolService(ctrl)
672+
mockNodePoolSvc.EXPECT().
673+
GetByIDAndOwner(gomock.Any(), nodePoolID, clusterID).
674+
Return(&api.NodePool{}, nil)
675+
mockNodePoolSvc.EXPECT().
676+
ForceDelete(gomock.Any(), nodePoolID, "some reason").
677+
Return(errors.GeneralError("failed to delete adapter statuses"))
636678
return mockClusterSvc, mockNodePoolSvc
637679
},
638-
expectedStatusCode: http.StatusBadRequest,
680+
expectedStatusCode: http.StatusInternalServerError,
639681
},
640682
}
641683

pkg/handlers/cluster_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func TestClusterHandler_ForceDelete(t *testing.T) {
173173
},
174174
{
175175
name: "Error 400 - reason exceeds max length",
176-
body: `{"reason": "` + strings.Repeat("x", 1025) + `"}`,
176+
body: `{"reason": "` + strings.Repeat("x", maxReasonLength+1) + `"}`,
177177
setupMocks: func(ctrl *gomock.Controller) (*services.MockClusterService, *services.MockGenericService) {
178178
mockClusterSvc := services.NewMockClusterService(ctrl)
179179
mockGenericSvc := services.NewMockGenericService(ctrl)

test/integration/node_pools_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,3 +960,124 @@ func TestNodePoolHardDelete(t *testing.T) {
960960
Expect(adapterStatuses).To(HaveLen(2))
961961
})
962962
}
963+
964+
func TestNodePoolForceDelete(t *testing.T) {
965+
t.Run("given a soft-deleted nodepool, when force-deleted, then nodepool and adapter statuses are removed", func(t *testing.T) { //nolint:lll
966+
RegisterTestingT(t)
967+
h, client := test.RegisterIntegration(t)
968+
account := h.NewRandAccount()
969+
ctx := h.NewAuthenticatedContext(account)
970+
971+
cluster, err := h.Factories.NewClusters(h.NewID())
972+
Expect(err).NotTo(HaveOccurred())
973+
974+
nodePool, err := h.Factories.NewNodePools(h.NewID())
975+
Expect(err).NotTo(HaveOccurred())
976+
dbSession := h.DBFactory.New(ctx)
977+
err = dbSession.Model(nodePool).Update("owner_id", cluster.ID).Error
978+
Expect(err).NotTo(HaveOccurred())
979+
980+
// Soft-delete the nodepool to put it in Finalizing state
981+
delResp, err := client.DeleteNodePoolByIdWithResponse(ctx, cluster.ID, nodePool.ID, test.WithAuthToken(ctx))
982+
Expect(err).NotTo(HaveOccurred())
983+
Expect(delResp.StatusCode()).To(Equal(http.StatusAccepted))
984+
985+
// Report adapter statuses so there are records to cascade-delete
986+
newGeneration := delResp.JSON202.Generation
987+
statusInput := newAdapterStatusRequest(
988+
"validation",
989+
newGeneration,
990+
[]openapi.ConditionRequest{
991+
{
992+
Type: api.AdapterConditionTypeApplied,
993+
Status: openapi.AdapterConditionStatusFalse,
994+
Reason: util.PtrString("Stuck"),
995+
},
996+
{
997+
Type: api.AdapterConditionTypeAvailable,
998+
Status: openapi.AdapterConditionStatusFalse,
999+
Reason: util.PtrString("Stuck"),
1000+
},
1001+
{
1002+
Type: api.AdapterConditionTypeHealth,
1003+
Status: openapi.AdapterConditionStatusTrue,
1004+
Reason: util.PtrString("Healthy"),
1005+
},
1006+
},
1007+
nil,
1008+
)
1009+
statusResp, err := client.PutNodePoolStatusesWithResponse(
1010+
ctx, cluster.ID, nodePool.ID,
1011+
openapi.PutNodePoolStatusesJSONRequestBody(statusInput), test.WithAuthToken(ctx),
1012+
)
1013+
Expect(err).NotTo(HaveOccurred())
1014+
Expect(statusResp.StatusCode()).To(Equal(http.StatusCreated))
1015+
1016+
// Force-delete the nodepool
1017+
forceDeleteResp, err := client.ForceDeleteNodePoolWithResponse(
1018+
ctx, cluster.ID, nodePool.ID,
1019+
openapi.ForceDeleteNodePoolJSONRequestBody{Reason: "integration test - adapter stuck"},
1020+
test.WithAuthToken(ctx),
1021+
)
1022+
Expect(err).NotTo(HaveOccurred())
1023+
Expect(forceDeleteResp.StatusCode()).To(Equal(http.StatusNoContent))
1024+
1025+
// Verify nodepool is gone
1026+
var nodePoolCheck api.NodePool
1027+
dbErr := dbSession.First(&nodePoolCheck, "id = ?", nodePool.ID).Error
1028+
Expect(dbErr).To(HaveOccurred(), "Nodepool should be hard-deleted from DB after force-delete")
1029+
Expect(dbErr.Error()).To(ContainSubstring("record not found"))
1030+
1031+
// Verify adapter statuses are gone
1032+
var adapterStatuses []api.AdapterStatus
1033+
err = dbSession.Where("resource_type = ? AND resource_id = ?", api.ResourceTypeNodePool, nodePool.ID).
1034+
Find(&adapterStatuses).Error
1035+
Expect(err).NotTo(HaveOccurred())
1036+
Expect(adapterStatuses).To(BeEmpty())
1037+
1038+
// Verify parent cluster still exists
1039+
var clusterCheck api.Cluster
1040+
err = dbSession.First(&clusterCheck, "id = ?", cluster.ID).Error
1041+
Expect(err).NotTo(HaveOccurred())
1042+
})
1043+
1044+
errorCases := []struct {
1045+
name string
1046+
reason string
1047+
expectedStatus int
1048+
createNodePool bool
1049+
}{
1050+
{"given empty reason, when force-deleted, then returns 400", "", http.StatusBadRequest, true},
1051+
{"given non-existent nodepool, when force-deleted, then returns 404", "should fail", http.StatusNotFound, false},
1052+
{"given nodepool not in Finalizing, when force-deleted, then returns 409", "should fail", http.StatusConflict, true},
1053+
}
1054+
1055+
for _, tc := range errorCases {
1056+
t.Run(tc.name, func(t *testing.T) {
1057+
RegisterTestingT(t)
1058+
h, client := test.RegisterIntegration(t)
1059+
account := h.NewRandAccount()
1060+
ctx := h.NewAuthenticatedContext(account)
1061+
1062+
cluster, err := h.Factories.NewClusters(h.NewID())
1063+
Expect(err).NotTo(HaveOccurred())
1064+
1065+
nodePoolID := h.NewID()
1066+
if tc.createNodePool {
1067+
nodePool, npErr := h.Factories.NewNodePools(h.NewID())
1068+
Expect(npErr).NotTo(HaveOccurred())
1069+
dbSession := h.DBFactory.New(ctx)
1070+
Expect(dbSession.Model(nodePool).Update("owner_id", cluster.ID).Error).NotTo(HaveOccurred())
1071+
nodePoolID = nodePool.ID
1072+
}
1073+
1074+
forceDeleteResp, err := client.ForceDeleteNodePoolWithResponse(
1075+
ctx, cluster.ID, nodePoolID,
1076+
openapi.ForceDeleteNodePoolJSONRequestBody{Reason: tc.reason},
1077+
test.WithAuthToken(ctx),
1078+
)
1079+
Expect(err).NotTo(HaveOccurred())
1080+
Expect(forceDeleteResp.StatusCode()).To(Equal(tc.expectedStatus))
1081+
})
1082+
}
1083+
}

0 commit comments

Comments
 (0)