package checks import ( "bytes" "context" "sort" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/influxdata/flux/ast" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/kit/platform" "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/mock" "github.com/influxdata/influxdb/v2/notification" "github.com/influxdata/influxdb/v2/notification/check" "github.com/influxdata/influxdb/v2/task/taskmodel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( orgOneID = "020f755c3c083000" orgTwoID = "020f755c3c083001" oneID = "020f755c3c082000" twoID = "020f755c3c082001" threeID = "020f755c3c082002" fourID = "020f755c3c082003" fiveID = "020f755c3c082004" sixID = "020f755c3c082005" checkOneID = "020f755c3c082000" checkTwoID = "020f755c3c082001" ) var script = `data = from(bucket: "telegraf") |> range(start: -1m) |> filter(fn: (r) => r._field == "usage_user")` var deadman1 = &check.Deadman{ Base: check.Base{ Name: "name1", ID: MustIDBase16(checkOneID), OrgID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(sixID), Description: "desc1", TaskID: 1, Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Buckets: []string{}, Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, Functions: []struct { Name string `json:"name"` }{}, }, }, Every: mustDuration("1m"), StatusMessageTemplate: "msg1", Tags: []influxdb.Tag{ {Key: "k1", Value: "v1"}, {Key: "k2", Value: "v2"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), }, }, TimeSince: mustDuration("21s"), StaleTime: mustDuration("1h"), ReportZero: true, Level: notification.Critical, } var threshold1 = &check.Threshold{ Base: check.Base{ Name: "name2", ID: MustIDBase16(checkTwoID), OrgID: MustIDBase16(orgTwoID), OwnerID: MustIDBase16(sixID), TaskID: 1, Description: "desc2", StatusMessageTemplate: "msg2", Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Buckets: []string{}, Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{}, Functions: []struct { Name string `json:"name"` }{}, }, }, Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), }, }, Thresholds: []check.ThresholdConfig{ &check.Lesser{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Ok, }, Value: 1000, }, &check.Greater{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Warn, }, Value: 2000, }, &check.Range{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Info, }, Min: 1500, Max: 1900, Within: true, }, }, } var checkCmpOptions = cmp.Options{ cmp.Comparer(func(x, y []byte) bool { return bytes.Equal(x, y) }), cmpopts.IgnoreFields(check.Base{}, "TaskID"), cmp.Transformer("Sort", func(in []influxdb.Check) []influxdb.Check { out := append([]influxdb.Check(nil), in...) // Copy input to avoid mutating it sort.Slice(out, func(i, j int) bool { return out[i].GetID() > out[j].GetID() }) return out }), } var taskCmpOptions = cmp.Options{ cmp.Comparer(func(x, y []byte) bool { return bytes.Equal(x, y) }), // skip comparing permissions cmpopts.IgnoreFields( taskmodel.Task{}, "LatestCompleted", "LatestScheduled", "CreatedAt", "UpdatedAt", ), cmp.Transformer("Sort", func(in []*taskmodel.Task) []*taskmodel.Task { out := append([]*taskmodel.Task{}, in...) // Copy input to avoid mutating it sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) return out }), } // CheckFields will include the IDGenerator, and checks type CheckFields struct { IDGenerator platform.IDGenerator TimeGenerator influxdb.TimeGenerator TaskService taskmodel.TaskService Checks []influxdb.Check Organizations []*influxdb.Organization Tasks []taskmodel.TaskCreate } type checkServiceFactory func(CheckFields, *testing.T) (influxdb.CheckService, taskmodel.TaskService, string, func()) type checkServiceF func( init checkServiceFactory, t *testing.T, ) // CheckService tests all the service functions. func CheckService( init checkServiceFactory, t *testing.T, ) { tests := []struct { name string fn checkServiceF }{ { name: "CreateCheck", fn: CreateCheck, }, { name: "FindCheckByID", fn: FindCheckByID, }, { name: "FindChecks", fn: FindChecks, }, { name: "FindCheck", fn: FindCheck, }, { name: "PatchCheck", fn: PatchCheck, }, { name: "UpdateCheck", fn: UpdateCheck, }, { name: "DeleteCheck", fn: DeleteCheck, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.fn(init, t) }) } } // CreateCheck testing func CreateCheck( init checkServiceFactory, t *testing.T, ) { type args struct { userID platform.ID check influxdb.Check } type wants struct { err *errors.Error checks []influxdb.Check tasks []*taskmodel.Task } tests := []struct { name string fields CheckFields args args wants wants }{ { name: "create checks with empty set", fields: CheckFields{ IDGenerator: mock.NewIDGenerator(checkOneID, t), TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Checks: []influxdb.Check{}, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, }, args: args{ userID: MustIDBase16(twoID), check: &check.Deadman{ Base: check.Base{ Name: "name1", OrgID: MustIDBase16(orgOneID), Description: "desc1", Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, Every: mustDuration("1m"), StatusMessageTemplate: "msg1", Tags: []influxdb.Tag{ {Key: "k1", Value: "v1"}, {Key: "k2", Value: "v2"}, }, }, TimeSince: mustDuration("21s"), StaleTime: mustDuration("1h"), ReportZero: true, Level: notification.Critical, }, }, wants: wants{ tasks: []*taskmodel.Task{ { ID: MustIDBase16("020f755c3c082000"), Name: "name1", Type: "deadman", OrganizationID: MustIDBase16("020f755c3c083000"), Organization: "theorg", OwnerID: MustIDBase16("020f755c3c082001"), Status: "active", Flux: "import \"influxdata/influxdb/monitor\"\nimport \"experimental\"\nimport \"influxdata/influxdb/v1\"\n\ndata = from(bucket: \"telegraf\") |> range(start: -1h) |> filter(fn: (r) => r._field == \"usage_user\")\n\noption task = {name: \"name1\", every: 1m}\n\ncheck = {_check_id: \"020f755c3c082000\", _check_name: \"name1\", _type: \"deadman\", tags: {k1: \"v1\", k2: \"v2\"}}\ncrit = (r) => r[\"dead\"]\nmessageFn = (r) => \"msg1\"\n\ndata |> v1[\"fieldsAsCols\"]() |> monitor[\"deadman\"](t: experimental[\"subDuration\"](from: now(), d: 21s))\n |> monitor[\"check\"](data: check, messageFn: messageFn, crit: crit)", Every: "1m", }, }, checks: []influxdb.Check{ &check.Deadman{ Base: check.Base{ Name: "name1", ID: MustIDBase16(checkOneID), OrgID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(twoID), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Buckets: []string{}, Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, Functions: []struct { Name string `json:"name"` }{}, }, }, Every: mustDuration("1m"), Description: "desc1", StatusMessageTemplate: "msg1", Tags: []influxdb.Tag{ {Key: "k1", Value: "v1"}, {Key: "k2", Value: "v2"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), }, }, TimeSince: mustDuration("21s"), StaleTime: mustDuration("1h"), ReportZero: true, Level: notification.Critical, }, }, }, }, { name: "basic create check", fields: CheckFields{ IDGenerator: &mock.IDGenerator{ IDFn: func() platform.ID { return MustIDBase16(checkTwoID) }, }, TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Checks: []influxdb.Check{ deadman1, }, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "otherorg", ID: MustIDBase16(orgTwoID), }, }, }, args: args{ userID: MustIDBase16(sixID), check: &check.Threshold{ Base: check.Base{ Name: "name2", OrgID: MustIDBase16(orgTwoID), OwnerID: MustIDBase16(twoID), Description: "desc2", StatusMessageTemplate: "msg2", Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, }, Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, }, }, Thresholds: []check.ThresholdConfig{ &check.Lesser{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Ok, }, Value: 1000, }, &check.Greater{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Warn, }, Value: 2000, }, &check.Range{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Info, }, Min: 1500, Max: 1900, Within: true, }, }, }, }, wants: wants{ checks: []influxdb.Check{ deadman1, threshold1, }, tasks: []*taskmodel.Task{ { ID: MustIDBase16("020f755c3c082001"), Name: "name2", Type: "threshold", OrganizationID: MustIDBase16("020f755c3c083001"), Organization: "otherorg", OwnerID: MustIDBase16("020f755c3c082005"), Status: "active", Every: "1m", Flux: "import \"influxdata/influxdb/monitor\"\nimport \"influxdata/influxdb/v1\"\n\ndata = from(bucket: \"telegraf\") |> range(start: -1m) |> filter(fn: (r) => r._field == \"usage_user\")\n\noption task = {name: \"name2\", every: 1m}\n\ncheck = {_check_id: \"020f755c3c082001\", _check_name: \"name2\", _type: \"threshold\", tags: {k11: \"v11\"}}\nok = (r) => r[\"usage_user\"] < 1000.0\nwarn = (r) => r[\"usage_user\"] > 2000.0\ninfo = (r) => r[\"usage_user\"] < 1900.0 and r[\"usage_user\"] > 1500.0\nmessageFn = (r) => \"msg2\"\n\ndata |> v1[\"fieldsAsCols\"]() |> monitor[\"check\"](\n data: check,\n messageFn: messageFn,\n ok: ok,\n warn: warn,\n info: info,\n)", }, }, }, }, { name: "names should be unique within an organization", fields: CheckFields{ IDGenerator: &mock.IDGenerator{ IDFn: func() platform.ID { return MustIDBase16(checkTwoID) }, }, TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Checks: []influxdb.Check{ deadman1, }, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "otherorg", ID: MustIDBase16(orgTwoID), }, }, }, args: args{ userID: MustIDBase16(twoID), check: &check.Threshold{ Base: check.Base{ Name: "name1", OrgID: MustIDBase16(orgOneID), Description: "desc1", Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, StatusMessageTemplate: "msg1", Tags: []influxdb.Tag{ {Key: "k1", Value: "v1"}, {Key: "k2", Value: "v2"}, }, }, }, }, wants: wants{ checks: []influxdb.Check{ deadman1, }, err: &errors.Error{ Code: errors.EConflict, Op: influxdb.OpCreateCheck, Msg: "check is not unique", }, }, }, { name: "names should not be unique across organizations", fields: CheckFields{ IDGenerator: &mock.IDGenerator{ IDFn: func() platform.ID { return MustIDBase16(checkTwoID) }, }, TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "otherorg", ID: MustIDBase16(orgTwoID), }, }, Checks: []influxdb.Check{ deadman1, }, }, args: args{ userID: MustIDBase16(twoID), check: &check.Threshold{ Base: check.Base{ Name: "name1", OrgID: MustIDBase16(orgTwoID), Description: "desc2", Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, StatusMessageTemplate: "msg2", Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, {Key: "k22", Value: "v22"}, }, }, }, }, wants: wants{ tasks: []*taskmodel.Task{ { ID: MustIDBase16("020f755c3c082001"), Name: "name1", Type: "threshold", OrganizationID: MustIDBase16("020f755c3c083001"), Organization: "otherorg", OwnerID: MustIDBase16("020f755c3c082001"), Status: "active", Every: "1m", Flux: "import \"influxdata/influxdb/monitor\"\nimport \"influxdata/influxdb/v1\"\n\ndata = from(bucket: \"telegraf\") |> range(start: -1m) |> filter(fn: (r) => r._field == \"usage_user\")\n\noption task = {name: \"name1\", every: 1m}\n\ncheck = {_check_id: \"020f755c3c082001\", _check_name: \"name1\", _type: \"threshold\", tags: {k11: \"v11\", k22: \"v22\"}}\nmessageFn = (r) => \"msg2\"\n\ndata |> v1[\"fieldsAsCols\"]() |> monitor[\"check\"](data: check, messageFn: messageFn)", }, }, checks: []influxdb.Check{ deadman1, &check.Threshold{ Base: check.Base{ ID: MustIDBase16(checkTwoID), Name: "name1", OrgID: MustIDBase16(orgTwoID), Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Buckets: []string{}, Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, Functions: []struct { Name string `json:"name"` }{}, }, }, OwnerID: MustIDBase16(twoID), Description: "desc2", StatusMessageTemplate: "msg2", Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, {Key: "k22", Value: "v22"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), }, }, }, }, }, }, { name: "create check with orgID not exist", fields: CheckFields{ IDGenerator: mock.NewIDGenerator(checkOneID, t), TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Checks: []influxdb.Check{}, Organizations: []*influxdb.Organization{}, }, args: args{ userID: MustIDBase16(twoID), check: &check.Threshold{ Base: check.Base{ Name: "name1", OrgID: MustIDBase16(orgOneID), Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, Description: "desc2", StatusMessageTemplate: "msg2", Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, {Key: "k22", Value: "v22"}, }, }, }, }, wants: wants{ checks: []influxdb.Check{}, err: &errors.Error{ Code: errors.ENotFound, Msg: "organization not found", Op: influxdb.OpCreateCheck, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, tasks, _, done := init(tt.fields, t) defer done() ctx := context.Background() createCheck := influxdb.CheckCreate{Check: tt.args.check, Status: influxdb.Active} err := s.CreateCheck(ctx, createCheck, tt.args.userID) influxErrsEqual(t, tt.wants.err, err) defer s.DeleteCheck(ctx, tt.args.check.GetID()) checks, _, err := s.FindChecks(ctx, influxdb.CheckFilter{}) if err != nil { t.Fatalf("failed to retrieve checks: %v", err) } if diff := cmp.Diff(checks, tt.wants.checks, checkCmpOptions...); diff != "" { t.Errorf("checks are different -got/+want\ndiff %s", diff) } foundTasks, _, err := tasks.FindTasks(ctx, taskmodel.TaskFilter{}) if err != nil { t.Fatal(err) } if diff := cmp.Diff(foundTasks, tt.wants.tasks, taskCmpOptions); diff != "" { t.Errorf("tasks are different -got/+want\ndiff %s", diff) } }) } } // FindCheckByID testing func FindCheckByID( init checkServiceFactory, t *testing.T, ) { type args struct { id platform.ID } type wants struct { err *errors.Error check influxdb.Check } tests := []struct { name string fields CheckFields args args wants wants }{ { name: "basic find check by id", fields: CheckFields{ Checks: []influxdb.Check{ deadman1, threshold1, }, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, }, args: args{ id: MustIDBase16(checkTwoID), }, wants: wants{ check: threshold1, }, }, { name: "find check by id not exist", fields: CheckFields{ Checks: []influxdb.Check{ deadman1, threshold1, }, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, }, args: args{ id: MustIDBase16(threeID), }, wants: wants{ err: &errors.Error{ Code: errors.ENotFound, Op: influxdb.OpFindCheckByID, Msg: "check not found", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, _, _, done := init(tt.fields, t) defer done() ctx := context.Background() check, err := s.FindCheckByID(ctx, tt.args.id) influxErrsEqual(t, tt.wants.err, err) if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" { t.Errorf("check is different -got/+want\ndiff %s", diff) } }) } } // FindChecks testing func FindChecks( init checkServiceFactory, t *testing.T, ) { type args struct { ID platform.ID name string organization string OrgID platform.ID userID platform.ID findOptions influxdb.FindOptions } type wants struct { checks []influxdb.Check err error } tests := []struct { name string fields CheckFields args args wants wants }{ { name: "find all checks", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "otherorg", ID: MustIDBase16(orgTwoID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ userID: MustIDBase16(sixID), }, wants: wants{ checks: []influxdb.Check{ deadman1, threshold1, }, }, }, { name: "find all checks by offset and limit", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ findOptions: influxdb.FindOptions{ Offset: 1, Limit: 1, }, }, wants: wants{ checks: []influxdb.Check{ threshold1, }, }, }, { name: "find all checks by descending", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ userID: MustIDBase16(sixID), findOptions: influxdb.FindOptions{ Limit: 1, Descending: true, }, }, wants: wants{ checks: []influxdb.Check{ threshold1, }, }, }, { name: "find checks by organization name", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "otherorg", ID: MustIDBase16(orgTwoID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ userID: MustIDBase16(sixID), organization: "theorg", }, wants: wants{ checks: []influxdb.Check{ deadman1, }, }, }, { name: "find checks by organization id", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "otherorg", ID: MustIDBase16(orgTwoID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ userID: MustIDBase16(sixID), OrgID: MustIDBase16(orgTwoID), }, wants: wants{ checks: []influxdb.Check{ threshold1, }, }, }, { name: "find check by name", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ userID: MustIDBase16(sixID), name: "name2", }, wants: wants{ checks: []influxdb.Check{ threshold1, }, }, }, { name: "missing check returns no checks", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{}, }, args: args{ userID: MustIDBase16(sixID), name: "xyz", }, wants: wants{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, _, opPrefix, done := init(tt.fields, t) defer done() ctx := context.Background() filter := influxdb.CheckFilter{} if tt.args.ID.Valid() { filter.ID = &tt.args.ID } if tt.args.OrgID.Valid() { filter.OrgID = &tt.args.OrgID } if tt.args.organization != "" { filter.Org = &tt.args.organization } if tt.args.name != "" { filter.Name = &tt.args.name } checks, _, err := s.FindChecks(ctx, filter, tt.args.findOptions) diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t) if diff := cmp.Diff(checks, tt.wants.checks, checkCmpOptions...); diff != "" { t.Errorf("checks are different -got/+want\ndiff %s", diff) } }) } } // DeleteCheck testing func DeleteCheck( init checkServiceFactory, t *testing.T, ) { type args struct { ID string userID platform.ID } type wants struct { err *errors.Error checks []influxdb.Check } tests := []struct { name string fields CheckFields args args wants wants }{ { name: "delete checks using exist id", fields: CheckFields{ IDGenerator: mock.NewIDGenerator("0000000000000001", t), Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Tasks: []taskmodel.TaskCreate{ { Flux: `option task = { every: 10s, name: "foo" } data = from(bucket: "telegraf") |> range(start: -1m)`, OrganizationID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(sixID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ ID: checkOneID, userID: MustIDBase16(sixID), }, wants: wants{ checks: []influxdb.Check{ threshold1, }, }, }, { name: "delete checks using id that does not exist", fields: CheckFields{ IDGenerator: mock.NewIDGenerator("0000000000000001", t), Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Tasks: []taskmodel.TaskCreate{ { Flux: `option task = { every: 10s, name: "foo" } data = from(bucket: "telegraf") |> range(start: -1m)`, OrganizationID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(sixID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ ID: "1234567890654321", userID: MustIDBase16(sixID), }, wants: wants{ err: &errors.Error{ Op: influxdb.OpDeleteCheck, Msg: "check not found", Code: errors.ENotFound, }, checks: []influxdb.Check{ deadman1, threshold1, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, _, _, done := init(tt.fields, t) defer done() ctx := context.Background() err := s.DeleteCheck(ctx, MustIDBase16(tt.args.ID)) influxErrsEqual(t, tt.wants.err, err) filter := influxdb.CheckFilter{} checks, _, err := s.FindChecks(ctx, filter) if err != nil { t.Fatalf("failed to retrieve checks: %v", err) } if diff := cmp.Diff(checks, tt.wants.checks, checkCmpOptions...); diff != "" { t.Errorf("checks are different -got/+want\ndiff %s", diff) } }) } } // FindCheck testing func FindCheck( init checkServiceFactory, t *testing.T, ) { type args struct { name string OrgID platform.ID } type wants struct { check influxdb.Check err *errors.Error } tests := []struct { name string fields CheckFields args args wants wants }{ { name: "find check by name", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, { Name: "theorg2", ID: MustIDBase16(orgTwoID), }, }, Checks: []influxdb.Check{ deadman1, threshold1, }, }, args: args{ name: "name1", OrgID: MustIDBase16(orgOneID), }, wants: wants{ check: deadman1, }, }, { name: "mixed filter", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{}, }, args: args{ name: "name2", OrgID: MustIDBase16(orgOneID), }, wants: wants{ err: &errors.Error{ Code: errors.ENotFound, Op: influxdb.OpFindCheck, Msg: "check not found", }, }, }, { name: "missing check returns error", fields: CheckFields{ Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{}, }, args: args{ name: "xyz", OrgID: MustIDBase16(orgOneID), }, wants: wants{ err: &errors.Error{ Code: errors.ENotFound, Op: influxdb.OpFindCheck, Msg: "check not found", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, _, _, done := init(tt.fields, t) defer done() var filter influxdb.CheckFilter if tt.args.name != "" { filter.Name = &tt.args.name } if tt.args.OrgID.Valid() { filter.OrgID = &tt.args.OrgID } check, err := s.FindCheck(context.Background(), filter) influxErrsEqual(t, tt.wants.err, err) if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" { t.Errorf("checks are different -got/+want\ndiff %s", diff) } }) } } // UpdateCheck testing func UpdateCheck( init checkServiceFactory, t *testing.T, ) { type args struct { id platform.ID check influxdb.Check } type wants struct { err error check influxdb.Check } tests := []struct { name string fields CheckFields args args wants wants }{ { name: "mixed update", fields: CheckFields{ IDGenerator: mock.NewIDGenerator("0000000000000001", t), TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC)}, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Tasks: []taskmodel.TaskCreate{ { Flux: `option task = { every: 10s, name: "foo" } data = from(bucket: "telegraf") |> range(start: -1m)`, OrganizationID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(sixID), }, }, Checks: []influxdb.Check{ deadman1, }, }, args: args{ id: MustIDBase16(checkOneID), check: &check.Threshold{ Base: check.Base{ ID: MustIDBase16(checkTwoID), OrgID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(twoID), Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, Name: "changed", Description: "desc changed", StatusMessageTemplate: "msg2", TaskID: 1, Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, {Key: "k22", Value: "v22"}, {Key: "k33", Value: "v33"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2001, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2002, 5, 4, 1, 2, 3, 0, time.UTC), }, }, Thresholds: []check.ThresholdConfig{ &check.Lesser{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Ok, }, Value: 1000, }, &check.Greater{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Warn, }, Value: 2000, }, &check.Range{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Info, }, Min: 1500, Max: 1900, Within: true, }, }, }, }, wants: wants{ check: &check.Threshold{ Base: check.Base{ ID: MustIDBase16(checkOneID), OrgID: MustIDBase16(orgOneID), Name: "changed", Every: mustDuration("1m"), OwnerID: MustIDBase16(sixID), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, Description: "desc changed", StatusMessageTemplate: "msg2", Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, {Key: "k22", Value: "v22"}, {Key: "k33", Value: "v33"}, }, TaskID: 1, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC), }, }, Thresholds: []check.ThresholdConfig{ &check.Lesser{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Ok, }, Value: 1000, }, &check.Greater{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Warn, }, Value: 2000, }, &check.Range{ ThresholdConfigBase: check.ThresholdConfigBase{ Level: notification.Info, }, Min: 1500, Max: 1900, Within: true, }, }, }, }, }, { name: "update name unique", fields: CheckFields{ TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{ deadman1, &check.Deadman{ Base: check.Base{ ID: MustIDBase16(checkTwoID), OrgID: MustIDBase16(orgOneID), Every: mustDuration("1m"), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, TaskID: 1, Name: "check2", OwnerID: MustIDBase16(twoID), StatusMessageTemplate: "msg1", }, }, }, }, args: args{ id: MustIDBase16(checkOneID), check: &check.Deadman{ Base: check.Base{ OrgID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(twoID), Name: "check2", Description: "desc changed", TaskID: 1, Every: mustDuration("1m"), StatusMessageTemplate: "msg2", Tags: []influxdb.Tag{ {Key: "k11", Value: "v11"}, {Key: "k22", Value: "v22"}, {Key: "k33", Value: "v33"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2001, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2002, 5, 4, 1, 2, 3, 0, time.UTC), }, }, TimeSince: mustDuration("12s"), StaleTime: mustDuration("1h"), ReportZero: false, Level: notification.Warn, }, }, wants: wants{ err: &errors.Error{ Code: errors.EConflict, Msg: "check name is not unique", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, _, opPrefix, done := init(tt.fields, t) defer done() ctx := context.Background() checkCreate := influxdb.CheckCreate{Check: tt.args.check, Status: influxdb.Active} check, err := s.UpdateCheck(ctx, tt.args.id, checkCreate) diffPlatformErrors(tt.name, err, tt.wants.err, opPrefix, t) if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" { t.Errorf("check is different -got/+want\ndiff %s", diff) } }) } } // PatchCheck testing func PatchCheck( init checkServiceFactory, t *testing.T, ) { type args struct { id platform.ID upd influxdb.CheckUpdate } type wants struct { err *errors.Error check influxdb.Check } inactive := influxdb.Inactive tests := []struct { name string fields CheckFields args args wants wants }{ { name: "mixed patch", fields: CheckFields{ IDGenerator: mock.NewIDGenerator("0000000000000001", t), TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC)}, Tasks: []taskmodel.TaskCreate{ { Flux: `option task = { every: 10s, name: "foo" } data = from(bucket: "telegraf") |> range(start: -1m)`, OrganizationID: MustIDBase16(orgOneID), OwnerID: MustIDBase16(sixID), }, }, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{ deadman1, }, }, args: args{ id: MustIDBase16(checkOneID), upd: influxdb.CheckUpdate{ Name: strPtr("changed"), Description: strPtr("desc changed"), Status: &inactive, }, }, wants: wants{ check: &check.Deadman{ Base: check.Base{ ID: MustIDBase16(checkOneID), OrgID: MustIDBase16(orgOneID), Name: "changed", OwnerID: MustIDBase16(sixID), Every: mustDuration("1m"), Description: "desc changed", Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Buckets: []string{}, Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, Functions: []struct { Name string `json:"name"` }{}, }, }, StatusMessageTemplate: "msg1", Tags: []influxdb.Tag{ {Key: "k1", Value: "v1"}, {Key: "k2", Value: "v2"}, }, CRUDLog: influxdb.CRUDLog{ CreatedAt: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC), UpdatedAt: time.Date(2007, 5, 4, 1, 2, 3, 0, time.UTC), }, }, TimeSince: mustDuration("21s"), StaleTime: mustDuration("1h"), ReportZero: true, Level: notification.Critical, }, }, }, { name: "update name unique", fields: CheckFields{ TimeGenerator: mock.TimeGenerator{FakeValue: time.Date(2006, 5, 4, 1, 2, 3, 0, time.UTC)}, Organizations: []*influxdb.Organization{ { Name: "theorg", ID: MustIDBase16(orgOneID), }, }, Checks: []influxdb.Check{ deadman1, &check.Deadman{ Base: check.Base{ ID: MustIDBase16(checkTwoID), OrgID: MustIDBase16(orgOneID), Every: mustDuration("1m"), Name: "check2", OwnerID: MustIDBase16(sixID), Query: influxdb.DashboardQuery{ Text: script, BuilderConfig: influxdb.BuilderConfig{ Tags: []struct { Key string `json:"key"` Values []string `json:"values"` AggregateFunctionType string `json:"aggregateFunctionType"` }{ { Key: "_field", Values: []string{"usage_user"}, AggregateFunctionType: "filter", }, }, }, }, StatusMessageTemplate: "msg1", }, }, }, }, args: args{ id: MustIDBase16(checkOneID), upd: influxdb.CheckUpdate{ Name: strPtr("check2"), }, }, wants: wants{ err: &errors.Error{ Code: errors.EConflict, Msg: "check entity update conflicts with an existing entity", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, _, _, done := init(tt.fields, t) defer done() ctx := context.Background() check, err := s.PatchCheck(ctx, tt.args.id, tt.args.upd) influxErrsEqual(t, tt.wants.err, err) if diff := cmp.Diff(check, tt.wants.check, checkCmpOptions...); diff != "" { t.Errorf("check is different -got/+want\ndiff %s", diff) } }) } } // MustIDBase16 is an helper to ensure a correct ID is built during testing. func MustIDBase16(s string) platform.ID { id, err := platform.IDFromString(s) if err != nil { panic(err) } return *id } func diffPlatformErrors(name string, actual, expected error, opPrefix string, t *testing.T) { t.Helper() ErrorsEqual(t, actual, expected) } // ErrorsEqual checks to see if the provided errors are equivalent. func ErrorsEqual(t *testing.T, actual, expected error) { t.Helper() if expected == nil && actual == nil { return } if expected == nil && actual != nil { t.Errorf("unexpected error %s", actual.Error()) } if expected != nil && actual == nil { t.Errorf("expected error %s but received nil", expected.Error()) } if errors.ErrorCode(expected) != errors.ErrorCode(actual) { t.Logf("\nexpected: %v\nactual: %v\n\n", expected, actual) t.Errorf("expected error code %q but received %q", errors.ErrorCode(expected), errors.ErrorCode(actual)) } if errors.ErrorMessage(expected) != errors.ErrorMessage(actual) { t.Logf("\nexpected: %v\nactual: %v\n\n", expected, actual) t.Errorf("expected error message %q but received %q", errors.ErrorMessage(expected), errors.ErrorMessage(actual)) } } func influxErrsEqual(t *testing.T, expected *errors.Error, actual error) { t.Helper() if expected != nil { require.Error(t, actual) } if actual == nil { return } if expected == nil { require.NoError(t, actual) return } iErr, ok := actual.(*errors.Error) require.True(t, ok) assert.Equal(t, expected.Code, iErr.Code) assert.Truef(t, strings.HasPrefix(iErr.Error(), expected.Error()), "expected: %s got err: %s", expected.Error(), actual.Error()) } func mustDuration(d string) *notification.Duration { dur, err := time.ParseDuration(d) if err != nil { panic(err) } ndur, err := notification.FromTimeDuration(dur) if err != nil { panic(err) } // Filter out the zero values from the duration. durs := make([]ast.Duration, 0, len(ndur.Values)) for _, d := range ndur.Values { if d.Magnitude != 0 { durs = append(durs, d) } } ndur.Values = durs return &ndur }