package pkger_test import ( "bytes" "context" "errors" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/influxdata/influxdb/v2/kit/platform" errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/pkg/testttp" "github.com/influxdata/influxdb/v2/pkger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" ) func TestPkgerHTTPServerStacks(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { b, err := os.ReadFile(strings.TrimPrefix(r.URL.Path, "/")) if err != nil { http.Error(w, err.Error(), 500) return } w.Write(b) }) filesvr := httptest.NewServer(mux) defer filesvr.Close() strPtr := func(s string) *string { return &s } newStackEvent := func(id platform.ID, k pkger.Kind, metaName string, associations ...pkger.RespStackResourceAssoc) pkger.RespStackResource { if associations == nil { associations = []pkger.RespStackResourceAssoc{} } return pkger.RespStackResource{ APIVersion: pkger.APIVersion, ID: id.String(), Kind: k, MetaName: metaName, Associations: associations, Links: stackResLinks(string(k.ResourceType()), id), } } t.Run("create a stack", func(t *testing.T) { t.Run("should successfully return with valid req body", func(t *testing.T) { svc := &fakeSVC{ initStackFn: func(ctx context.Context, userID platform.ID, stackCr pkger.StackCreate) (pkger.Stack, error) { return pkger.Stack{ ID: 2, OrgID: stackCr.OrgID, Events: []pkger.StackEvent{ { Name: stackCr.Name, Description: stackCr.Description, Sources: stackCr.Sources, TemplateURLs: stackCr.TemplateURLs, UpdatedAt: time.Now(), }, }, CreatedAt: time.Now(), }, nil }, } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) reqBody := pkger.ReqCreateStack{ OrgID: platform.ID(3).String(), Name: "threeve", Description: "desc", URLs: []string{"http://example.com"}, } testttp. PostJSON(t, "/api/v2/stacks", reqBody). Headers("Content-Type", "application/json"). Do(svr). ExpectStatus(http.StatusCreated). ExpectBody(func(buf *bytes.Buffer) { var resp pkger.RespStack decodeBody(t, buf, &resp) assert.NotZero(t, resp.ID) assert.Equal(t, pkger.StackEventCreate.String(), resp.EventType) assert.Equal(t, reqBody.OrgID, resp.OrgID) assert.Equal(t, reqBody.Name, resp.Name) assert.Equal(t, reqBody.Description, resp.Description) assert.Equal(t, reqBody.URLs, resp.URLs) assert.NotZero(t, resp.CreatedAt) assert.NotZero(t, resp.UpdatedAt) }) }) t.Run("error cases", func(t *testing.T) { tests := []struct { name string reqBody pkger.ReqCreateStack expectedStatus int svc pkger.SVC }{ { name: "bad org id", reqBody: pkger.ReqCreateStack{ OrgID: "invalid id", }, expectedStatus: http.StatusBadRequest, }, { name: "bad url", reqBody: pkger.ReqCreateStack{ OrgID: platform.ID(3).String(), URLs: []string{"invalid @% url"}, }, expectedStatus: http.StatusBadRequest, }, { name: "translates svc conflict error", reqBody: pkger.ReqCreateStack{OrgID: platform.ID(3).String()}, svc: &fakeSVC{ initStackFn: func(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) { return pkger.Stack{}, &errors2.Error{Code: errors2.EConflict} }, }, expectedStatus: http.StatusUnprocessableEntity, }, { name: "translates svc internal error", reqBody: pkger.ReqCreateStack{OrgID: platform.ID(3).String()}, svc: &fakeSVC{ initStackFn: func(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) { return pkger.Stack{}, &errors2.Error{Code: errors2.EInternal} }, }, expectedStatus: http.StatusInternalServerError, }, } for _, tt := range tests { fn := func(t *testing.T) { svc := tt.svc if svc == nil { svc = &fakeSVC{ initStackFn: func(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) { return pkger.Stack{}, nil }, } } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) testttp. PostJSON(t, "/api/v2/stacks", tt.reqBody). Headers("Content-Type", "application/json"). Do(svr). ExpectStatus(tt.expectedStatus) } t.Run(tt.name, fn) } }) }) t.Run("list a stack", func(t *testing.T) { t.Run("should successfully return with valid req body", func(t *testing.T) { const expectedOrgID platform.ID = 3 svc := &fakeSVC{ listStacksFn: func(ctx context.Context, orgID platform.ID, filter pkger.ListFilter) ([]pkger.Stack, error) { if orgID != expectedOrgID { return nil, nil } if len(filter.Names) > 0 && len(filter.StackIDs) == 0 { var stacks []pkger.Stack for i, name := range filter.Names { stacks = append(stacks, pkger.Stack{ ID: platform.ID(i + 1), OrgID: expectedOrgID, Events: []pkger.StackEvent{ { Name: name, }, }, }) } return stacks, nil } if len(filter.StackIDs) > 0 && len(filter.Names) == 0 { var stacks []pkger.Stack for _, stackID := range filter.StackIDs { stacks = append(stacks, pkger.Stack{ ID: stackID, OrgID: expectedOrgID, Events: []pkger.StackEvent{{}}, }) } return stacks, nil } return []pkger.Stack{{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Name: "stack_1", }}, }}, nil }, } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) tests := []struct { name string queryArgs string expectedStacks []pkger.RespStack }{ { name: "with org ID that has stacks", queryArgs: "orgID=" + expectedOrgID.String(), expectedStacks: []pkger.RespStack{{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Name: "stack_1", Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Name: "stack_1", Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, }, }}, }, { name: "with orgID with no stacks", queryArgs: "orgID=" + platform.ID(9000).String(), expectedStacks: []pkger.RespStack{}, }, { name: "with names", queryArgs: "name=name_stack&name=threeve&orgID=" + expectedOrgID.String(), expectedStacks: []pkger.RespStack{ { ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Name: "name_stack", Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Name: "name_stack", Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, }, }, { ID: platform.ID(2).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Name: "threeve", Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Name: "threeve", Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, }, }, }, }, { name: "with ids", queryArgs: fmt.Sprintf("stackID=%s&stackID=%s&orgID=%s", platform.ID(1), platform.ID(2), platform.ID(expectedOrgID)), expectedStacks: []pkger.RespStack{ { ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, }, }, { ID: platform.ID(2).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Resources: []pkger.RespStackResource{}, Sources: []string{}, URLs: []string{}, }, }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { testttp. Get(t, "/api/v2/stacks?"+tt.queryArgs). Headers("Content-Type", "application/x-www-form-urlencoded"). Do(svr). ExpectStatus(http.StatusOK). ExpectBody(func(buf *bytes.Buffer) { var resp pkger.RespListStacks decodeBody(t, buf, &resp) assert.Equal(t, tt.expectedStacks, resp.Stacks) }) } t.Run(tt.name, fn) } }) t.Run("should provide all resource links for each stack resource collection", func(t *testing.T) { const expectedOrgID platform.ID = 3 tests := []struct { name string stub pkger.Stack expectedStack pkger.RespStack }{ { name: "for stacks with associated buckets", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindBucket, MetaName: "buck-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindBucket, "buck-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindBucket, "buck-1"), }, }, }, }, }, { name: "for stacks with associated checks", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindCheckThreshold, MetaName: "check-thresh", }, { APIVersion: pkger.APIVersion, ID: 2, Kind: pkger.KindCheckDeadman, MetaName: "check-deadman", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindCheckThreshold, "check-thresh"), newStackEvent(2, pkger.KindCheckDeadman, "check-deadman"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindCheckThreshold, "check-thresh"), newStackEvent(2, pkger.KindCheckDeadman, "check-deadman"), }, }, }, }, }, { name: "for stacks with associated dashboards", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindDashboard, MetaName: "dash", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindDashboard, "dash"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindDashboard, "dash"), }, }, }, }, }, { name: "for stacks with associated labels", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindLabel, MetaName: "label", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindLabel, "label"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindLabel, "label"), }, }, }, }, }, { name: "for stacks with associated notification endpoints", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindNotificationEndpoint, MetaName: "end-1", }, { APIVersion: pkger.APIVersion, ID: 2, Kind: pkger.KindNotificationEndpointHTTP, MetaName: "end-2", }, { APIVersion: pkger.APIVersion, ID: 3, Kind: pkger.KindNotificationEndpointPagerDuty, MetaName: "end-3", }, { APIVersion: pkger.APIVersion, ID: 4, Kind: pkger.KindNotificationEndpointSlack, MetaName: "end-4", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationEndpoint, "end-1"), newStackEvent(2, pkger.KindNotificationEndpointHTTP, "end-2"), newStackEvent(3, pkger.KindNotificationEndpointPagerDuty, "end-3"), newStackEvent(4, pkger.KindNotificationEndpointSlack, "end-4"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationEndpoint, "end-1"), newStackEvent(2, pkger.KindNotificationEndpointHTTP, "end-2"), newStackEvent(3, pkger.KindNotificationEndpointPagerDuty, "end-3"), newStackEvent(4, pkger.KindNotificationEndpointSlack, "end-4"), }, }, }, }, }, { name: "for stacks with associated notification rules", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindNotificationRule, MetaName: "rule-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationRule, "rule-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationRule, "rule-1"), }, }, }, }, }, { name: "for stacks with associated tasks", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindTask, MetaName: "task-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTask, "task-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTask, "task-1"), }, }, }, }, }, { name: "for stacks with associated telegraf configs", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindTelegraf, MetaName: "tele-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTelegraf, "tele-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTelegraf, "tele-1"), }, }, }, }, }, { name: "for stacks with associated variables", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindVariable, MetaName: "var-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindVariable, "var-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindVariable, "var-1"), }, }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { svc := &fakeSVC{ listStacksFn: func(ctx context.Context, orgID platform.ID, filter pkger.ListFilter) ([]pkger.Stack, error) { return []pkger.Stack{tt.stub}, nil }, } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) testttp. Get(t, "/api/v2/stacks?orgID="+expectedOrgID.String()). Do(svr). ExpectStatus(http.StatusOK). ExpectBody(func(buf *bytes.Buffer) { var resp pkger.RespListStacks decodeBody(t, buf, &resp) require.Len(t, resp.Stacks, 1) assert.Equal(t, tt.expectedStack, resp.Stacks[0]) }) } t.Run(tt.name, fn) } }) }) t.Run("read a stack", func(t *testing.T) { t.Run("should successfully return with valid req body", func(t *testing.T) { const expectedOrgID platform.ID = 3 tests := []struct { name string stub pkger.Stack expectedStack pkger.RespStack }{ { name: "for stack that has all fields available", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{ { Name: "name", Description: "desc", Sources: []string{"threeve"}, TemplateURLs: []string{"http://example.com"}, Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 3, Kind: pkger.KindBucket, MetaName: "rucketeer", }, }, }, }, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Name: "name", Description: "desc", Sources: []string{"threeve"}, URLs: []string{"http://example.com"}, Resources: []pkger.RespStackResource{ newStackEvent(3, pkger.KindBucket, "rucketeer"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Name: "name", Description: "desc", Sources: []string{"threeve"}, URLs: []string{"http://example.com"}, Resources: []pkger.RespStackResource{ newStackEvent(3, pkger.KindBucket, "rucketeer"), }, }, }, }, }, { name: "for stack that has missing resources urls and sources", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{ { Name: "name", Description: "desc", }, }, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Name: "name", Description: "desc", Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Name: "name", Description: "desc", Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, }, }, }, { name: "for stack that has no set fields", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{}}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, }, }, }, { name: "for stacks with associated checks", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindCheckThreshold, MetaName: "check-thresh", }, { APIVersion: pkger.APIVersion, ID: 2, Kind: pkger.KindCheckDeadman, MetaName: "check-deadman", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindCheckThreshold, "check-thresh"), newStackEvent(2, pkger.KindCheckDeadman, "check-deadman"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindCheckThreshold, "check-thresh"), newStackEvent(2, pkger.KindCheckDeadman, "check-deadman"), }, }, }, }, }, { name: "for stacks with associated dashboards", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindDashboard, MetaName: "dash", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindDashboard, "dash"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindDashboard, "dash"), }, }, }, }, }, { name: "for stacks with associated labels", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindLabel, MetaName: "label", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindLabel, "label"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindLabel, "label"), }, }, }, }, }, { name: "for stacks with associated notification endpoints", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindNotificationEndpoint, MetaName: "end-1", }, { APIVersion: pkger.APIVersion, ID: 2, Kind: pkger.KindNotificationEndpointHTTP, MetaName: "end-2", }, { APIVersion: pkger.APIVersion, ID: 3, Kind: pkger.KindNotificationEndpointPagerDuty, MetaName: "end-3", }, { APIVersion: pkger.APIVersion, ID: 4, Kind: pkger.KindNotificationEndpointSlack, MetaName: "end-4", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationEndpoint, "end-1"), newStackEvent(2, pkger.KindNotificationEndpointHTTP, "end-2"), newStackEvent(3, pkger.KindNotificationEndpointPagerDuty, "end-3"), newStackEvent(4, pkger.KindNotificationEndpointSlack, "end-4"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationEndpoint, "end-1"), newStackEvent(2, pkger.KindNotificationEndpointHTTP, "end-2"), newStackEvent(3, pkger.KindNotificationEndpointPagerDuty, "end-3"), newStackEvent(4, pkger.KindNotificationEndpointSlack, "end-4"), }, }, }, }, }, { name: "for stacks with associated notification rules", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindNotificationRule, MetaName: "rule-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationRule, "rule-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindNotificationRule, "rule-1"), }, }, }, }, }, { name: "for stacks with associated tasks", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindTask, MetaName: "task-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTask, "task-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTask, "task-1"), }, }, }, }, }, { name: "for stacks with associated telegraf configs", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindTelegraf, MetaName: "tele-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTelegraf, "tele-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindTelegraf, "tele-1"), }, }, }, }, }, { name: "for stacks with associated variables", stub: pkger.Stack{ ID: 1, OrgID: expectedOrgID, Events: []pkger.StackEvent{{ Resources: []pkger.StackResource{ { APIVersion: pkger.APIVersion, ID: 1, Kind: pkger.KindVariable, MetaName: "var-1", }, }, }}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindVariable, "var-1"), }, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventCreate.String(), Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{ newStackEvent(1, pkger.KindVariable, "var-1"), }, }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { svc := &fakeSVC{ readStackFn: func(ctx context.Context, id platform.ID) (pkger.Stack, error) { return tt.stub, nil }, } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) testttp. Get(t, "/api/v2/stacks/"+tt.stub.ID.String()). Do(svr). ExpectStatus(http.StatusOK). ExpectBody(func(buf *bytes.Buffer) { var resp pkger.RespStack decodeBody(t, buf, &resp) assert.Equal(t, tt.expectedStack, resp) }) } t.Run(tt.name, fn) } }) t.Run("error cases", func(t *testing.T) { tests := []struct { name string stackIDPath string expectedStatus int svc pkger.SVC }{ { name: "bad stack id path", stackIDPath: "badID", expectedStatus: http.StatusBadRequest, }, { name: "stack not found", stackIDPath: platform.ID(1).String(), svc: &fakeSVC{ readStackFn: func(ctx context.Context, id platform.ID) (pkger.Stack, error) { return pkger.Stack{}, &errors2.Error{Code: errors2.ENotFound} }, }, expectedStatus: http.StatusNotFound, }, } for _, tt := range tests { fn := func(t *testing.T) { svc := tt.svc if svc == nil { svc = &fakeSVC{ initStackFn: func(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) { return pkger.Stack{}, nil }, } } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) testttp. Get(t, "/api/v2/stacks/"+tt.stackIDPath). Headers("Content-Type", "application/json"). Do(svr). ExpectStatus(tt.expectedStatus) } t.Run(tt.name, fn) } }) }) t.Run("update a stack", func(t *testing.T) { t.Run("should successfully update with valid req body", func(t *testing.T) { const expectedOrgID platform.ID = 3 tests := []struct { name string input pkger.ReqUpdateStack expectedStack pkger.RespStack }{ { name: "update name field", input: pkger.ReqUpdateStack{ Name: strPtr("name"), }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventUpdate.String(), Name: "name", Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventUpdate.String(), Name: "name", Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, }, }, }, { name: "update desc field", input: pkger.ReqUpdateStack{ Description: strPtr("desc"), }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventUpdate.String(), Description: "desc", Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventUpdate.String(), Description: "desc", Sources: []string{}, URLs: []string{}, Resources: []pkger.RespStackResource{}, }, }, }, }, { name: "update urls field", input: pkger.ReqUpdateStack{ TemplateURLs: []string{"http://example.com"}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventUpdate.String(), Sources: []string{}, URLs: []string{"http://example.com"}, Resources: []pkger.RespStackResource{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventUpdate.String(), Sources: []string{}, URLs: []string{"http://example.com"}, Resources: []pkger.RespStackResource{}, }, }, }, }, { name: "update all fields", input: pkger.ReqUpdateStack{ Name: strPtr("name"), Description: strPtr("desc"), TemplateURLs: []string{"http://example.com"}, }, expectedStack: pkger.RespStack{ ID: platform.ID(1).String(), OrgID: expectedOrgID.String(), RespStackEvent: pkger.RespStackEvent{ EventType: pkger.StackEventUpdate.String(), Name: "name", Description: "desc", Sources: []string{}, URLs: []string{"http://example.com"}, Resources: []pkger.RespStackResource{}, }, Events: []pkger.RespStackEvent{ { EventType: pkger.StackEventUpdate.String(), Name: "name", Description: "desc", Sources: []string{}, URLs: []string{"http://example.com"}, Resources: []pkger.RespStackResource{}, }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { id, err := platform.IDFromString(tt.expectedStack.ID) require.NoError(t, err) svc := &fakeSVC{ updateStackFn: func(ctx context.Context, upd pkger.StackUpdate) (pkger.Stack, error) { if upd.ID != *id { return pkger.Stack{}, errors.New("unexpected stack ID: " + upd.ID.String()) } st := pkger.Stack{ ID: *id, OrgID: expectedOrgID, } ev := pkger.StackEvent{EventType: pkger.StackEventUpdate} if upd.Name != nil { ev.Name = *upd.Name } if upd.Description != nil { ev.Description = *upd.Description } if upd.TemplateURLs != nil { ev.TemplateURLs = upd.TemplateURLs } st.Events = append(st.Events, ev) return st, nil }, } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) testttp. PatchJSON(t, "/api/v2/stacks/"+tt.expectedStack.ID, tt.input). Do(svr). ExpectStatus(http.StatusOK). ExpectBody(func(buf *bytes.Buffer) { var resp pkger.RespStack decodeBody(t, buf, &resp) assert.Equal(t, tt.expectedStack, resp) }) } t.Run(tt.name, fn) } }) t.Run("error cases", func(t *testing.T) { tests := []struct { name string stackIDPath string expectedStatus int svc pkger.SVC }{ { name: "bad stack id path", stackIDPath: "badID", expectedStatus: http.StatusBadRequest, }, { name: "stack not found", stackIDPath: platform.ID(1).String(), svc: &fakeSVC{ readStackFn: func(ctx context.Context, id platform.ID) (pkger.Stack, error) { return pkger.Stack{}, &errors2.Error{Code: errors2.ENotFound} }, }, expectedStatus: http.StatusNotFound, }, } for _, tt := range tests { fn := func(t *testing.T) { svc := tt.svc if svc == nil { svc = &fakeSVC{ initStackFn: func(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) { return pkger.Stack{}, nil }, } } pkgHandler := pkger.NewHTTPServerStacks(zap.NewNop(), svc) svr := newMountedHandler(pkgHandler, 1) testttp. Get(t, "/api/v2/stacks/"+tt.stackIDPath). Headers("Content-Type", "application/json"). Do(svr). ExpectStatus(tt.expectedStatus) } t.Run(tt.name, fn) } }) }) } type fakeSVC struct { initStackFn func(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) listStacksFn func(ctx context.Context, orgID platform.ID, filter pkger.ListFilter) ([]pkger.Stack, error) readStackFn func(ctx context.Context, id platform.ID) (pkger.Stack, error) updateStackFn func(ctx context.Context, upd pkger.StackUpdate) (pkger.Stack, error) dryRunFn func(ctx context.Context, orgID, userID platform.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) applyFn func(ctx context.Context, orgID, userID platform.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) } var _ pkger.SVC = (*fakeSVC)(nil) func (f *fakeSVC) InitStack(ctx context.Context, userID platform.ID, stack pkger.StackCreate) (pkger.Stack, error) { if f.initStackFn == nil { panic("not implemented") } return f.initStackFn(ctx, userID, stack) } func (f *fakeSVC) UninstallStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID platform.ID }) (pkger.Stack, error) { panic("not implemented") } func (f *fakeSVC) DeleteStack(ctx context.Context, identifiers struct{ OrgID, UserID, StackID platform.ID }) error { panic("not implemented yet") } func (f *fakeSVC) ListStacks(ctx context.Context, orgID platform.ID, filter pkger.ListFilter) ([]pkger.Stack, error) { if f.listStacksFn == nil { panic("not implemented") } return f.listStacksFn(ctx, orgID, filter) } func (f *fakeSVC) ReadStack(ctx context.Context, id platform.ID) (pkger.Stack, error) { if f.readStackFn != nil { return f.readStackFn(ctx, id) } panic("not implemented") } func (f *fakeSVC) UpdateStack(ctx context.Context, upd pkger.StackUpdate) (pkger.Stack, error) { if f.updateStackFn != nil { return f.updateStackFn(ctx, upd) } panic("not implemented") } func (f *fakeSVC) Export(ctx context.Context, setters ...pkger.ExportOptFn) (*pkger.Template, error) { panic("not implemented") } func (f *fakeSVC) DryRun(ctx context.Context, orgID, userID platform.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) { if f.dryRunFn == nil { panic("not implemented") } return f.dryRunFn(ctx, orgID, userID, opts...) } func (f *fakeSVC) Apply(ctx context.Context, orgID, userID platform.ID, opts ...pkger.ApplyOptFn) (pkger.ImpactSummary, error) { if f.applyFn == nil { panic("not implemented") } return f.applyFn(ctx, orgID, userID, opts...) } func stackResLinks(resource string, id platform.ID) pkger.RespStackResourceLinks { return pkger.RespStackResourceLinks{ Self: fmt.Sprintf("/api/v2/%s/%s", resource, id), } }