package pkger import ( "bytes" "context" "errors" "fmt" "math/rand" "net/url" "regexp" "sort" "strconv" "testing" "time" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/kit/platform" errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors" "github.com/influxdata/influxdb/v2/mock" "github.com/influxdata/influxdb/v2/notification" icheck "github.com/influxdata/influxdb/v2/notification/check" "github.com/influxdata/influxdb/v2/notification/endpoint" "github.com/influxdata/influxdb/v2/notification/rule" "github.com/influxdata/influxdb/v2/task/taskmodel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) func TestService(t *testing.T) { newTestService := func(opts ...ServiceSetterFn) *Service { opt := serviceOpt{ bucketSVC: mock.NewBucketService(), checkSVC: mock.NewCheckService(), dashSVC: mock.NewDashboardService(), labelSVC: mock.NewLabelService(), endpointSVC: mock.NewNotificationEndpointService(), orgSVC: mock.NewOrganizationService(), ruleSVC: mock.NewNotificationRuleStore(), store: &fakeStore{ createFn: func(ctx context.Context, stack Stack) error { return nil }, deleteFn: func(ctx context.Context, id platform.ID) error { return nil }, readFn: func(ctx context.Context, id platform.ID) (Stack, error) { return Stack{ID: id}, nil }, updateFn: func(ctx context.Context, stack Stack) error { return nil }, }, taskSVC: mock.NewTaskService(), teleSVC: mock.NewTelegrafConfigStore(), varSVC: mock.NewVariableService(), } for _, o := range opts { o(&opt) } applyOpts := []ServiceSetterFn{ WithStore(opt.store), WithBucketSVC(opt.bucketSVC), WithCheckSVC(opt.checkSVC), WithDashboardSVC(opt.dashSVC), WithLabelSVC(opt.labelSVC), WithNotificationEndpointSVC(opt.endpointSVC), WithNotificationRuleSVC(opt.ruleSVC), WithOrganizationService(opt.orgSVC), WithSecretSVC(opt.secretSVC), WithTaskSVC(opt.taskSVC), WithTelegrafSVC(opt.teleSVC), WithVariableSVC(opt.varSVC), } if opt.idGen != nil { applyOpts = append(applyOpts, WithIDGenerator(opt.idGen)) } if opt.timeGen != nil { applyOpts = append(applyOpts, WithTimeGenerator(opt.timeGen)) } if opt.nameGen != nil { applyOpts = append(applyOpts, withNameGen(opt.nameGen)) } return NewService(applyOpts...) } t.Run("DryRun", func(t *testing.T) { type dryRunTestFields struct { path string kinds []Kind skipResources []ActionSkipResource assertFn func(*testing.T, ImpactSummary) } testDryRunActions := func(t *testing.T, fields dryRunTestFields) { t.Helper() var skipResOpts []ApplyOptFn for _, asr := range fields.skipResources { skipResOpts = append(skipResOpts, ApplyWithResourceSkip(asr)) } testfileRunner(t, fields.path, func(t *testing.T, template *Template) { t.Helper() tests := []struct { name string applyOpts []ApplyOptFn }{ { name: "skip resources", applyOpts: skipResOpts, }, } for _, k := range fields.kinds { tests = append(tests, struct { name string applyOpts []ApplyOptFn }{ name: "skip kind " + k.String(), applyOpts: []ApplyOptFn{ ApplyWithKindSkip(ActionSkipKind{ Kind: k, }), }, }) } for _, tt := range tests { fn := func(t *testing.T) { t.Helper() svc := newTestService() impact, err := svc.DryRun( context.TODO(), platform.ID(100), 0, append(tt.applyOpts, ApplyWithTemplate(template))..., ) require.NoError(t, err) fields.assertFn(t, impact) } t.Run(tt.name, fn) } }) } t.Run("buckets", func(t *testing.T) { t.Run("single bucket updated", func(t *testing.T) { testfileRunner(t, "testdata/bucket.yml", func(t *testing.T, template *Template) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(_ context.Context, orgID platform.ID, name string) (*influxdb.Bucket, error) { if name != "rucket-11" { return nil, errors.New("not found") } return &influxdb.Bucket{ ID: platform.ID(1), OrgID: orgID, Name: name, Description: "old desc", RetentionPeriod: 30 * time.Hour, }, nil } svc := newTestService(WithBucketSVC(fakeBktSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) require.Len(t, impact.Diff.Buckets, 2) expected := DiffBucket{ DiffIdentifier: DiffIdentifier{ ID: SafeID(1), StateStatus: StateStatusExists, MetaName: "rucket-11", Kind: KindBucket, }, Old: &DiffBucketValues{ Name: "rucket-11", Description: "old desc", RetentionRules: retentionRules{newRetentionRule(30 * time.Hour)}, }, New: DiffBucketValues{ Name: "rucket-11", Description: "bucket 1 description", RetentionRules: retentionRules{newRetentionRule(time.Hour)}, }, } assert.Contains(t, impact.Diff.Buckets, expected) }) }) t.Run("single bucket new", func(t *testing.T) { testfileRunner(t, "testdata/bucket.json", func(t *testing.T, template *Template) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(_ context.Context, orgID platform.ID, name string) (*influxdb.Bucket, error) { return nil, errors.New("not found") } svc := newTestService(WithBucketSVC(fakeBktSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) require.Len(t, impact.Diff.Buckets, 2) expected := DiffBucket{ DiffIdentifier: DiffIdentifier{ MetaName: "rucket-11", StateStatus: StateStatusNew, Kind: KindBucket, }, New: DiffBucketValues{ Name: "rucket-11", Description: "bucket 1 description", RetentionRules: retentionRules{newRetentionRule(time.Hour)}, }, } assert.Contains(t, impact.Diff.Buckets, expected) }) }) t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/bucket.yml", kinds: []Kind{KindBucket}, skipResources: []ActionSkipResource{ { Kind: KindBucket, MetaName: "rucket-22", }, { Kind: KindBucket, MetaName: "rucket-11", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Buckets) }, }) }) }) t.Run("checks", func(t *testing.T) { t.Run("mixed update and creates", func(t *testing.T) { testfileRunner(t, "testdata/checks.yml", func(t *testing.T, template *Template) { fakeCheckSVC := mock.NewCheckService() id := platform.ID(1) existing := &icheck.Deadman{ Base: icheck.Base{ ID: id, Name: "display name", Description: "old desc", }, } fakeCheckSVC.FindCheckFn = func(ctx context.Context, f influxdb.CheckFilter) (influxdb.Check, error) { if f.Name != nil && *f.Name == "display name" { return existing, nil } return nil, errors.New("not found") } svc := newTestService(WithCheckSVC(fakeCheckSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) checks := impact.Diff.Checks require.Len(t, checks, 2) check0 := checks[0] assert.True(t, check0.IsNew()) assert.Equal(t, "check-0", check0.MetaName) assert.Zero(t, check0.ID) assert.Nil(t, check0.Old) check1 := checks[1] assert.False(t, check1.IsNew()) assert.Equal(t, "check-1", check1.MetaName) assert.Equal(t, "display name", check1.New.GetName()) assert.NotZero(t, check1.ID) assert.Equal(t, existing, check1.Old.Check) }) }) t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/checks.yml", kinds: []Kind{KindCheck, KindCheckDeadman, KindCheckThreshold}, skipResources: []ActionSkipResource{ { Kind: KindCheck, MetaName: "check-0", }, { Kind: KindCheck, MetaName: "check-1", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Checks) }, }) }) }) t.Run("dashboards", func(t *testing.T) { t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/dashboard.yml", kinds: []Kind{KindDashboard}, skipResources: []ActionSkipResource{ { Kind: KindDashboard, MetaName: "dash-1", }, { Kind: KindDashboard, MetaName: "dash-2", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Dashboards) }, }) }) }) t.Run("labels", func(t *testing.T) { t.Run("two labels updated", func(t *testing.T) { testfileRunner(t, "testdata/label.json", func(t *testing.T, template *Template) { fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { return []*influxdb.Label{ { ID: platform.ID(1), Name: filter.Name, Properties: map[string]string{ "color": "old color", "description": "old description", }, }, }, nil } svc := newTestService(WithLabelSVC(fakeLabelSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) require.Len(t, impact.Diff.Labels, 3) expected := DiffLabel{ DiffIdentifier: DiffIdentifier{ ID: SafeID(1), StateStatus: StateStatusExists, MetaName: "label-1", Kind: KindLabel, }, Old: &DiffLabelValues{ Name: "label-1", Color: "old color", Description: "old description", }, New: DiffLabelValues{ Name: "label-1", Color: "#FFFFFF", Description: "label 1 description", }, } assert.Contains(t, impact.Diff.Labels, expected) expected.MetaName = "label-2" expected.New.Name = "label-2" expected.New.Color = "#000000" expected.New.Description = "label 2 description" expected.Old.Name = "label-2" assert.Contains(t, impact.Diff.Labels, expected) }) }) t.Run("two labels created", func(t *testing.T) { testfileRunner(t, "testdata/label.yml", func(t *testing.T, template *Template) { fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { return nil, errors.New("no labels found") } svc := newTestService(WithLabelSVC(fakeLabelSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) labels := impact.Diff.Labels require.Len(t, labels, 3) expected := DiffLabel{ DiffIdentifier: DiffIdentifier{ MetaName: "label-1", StateStatus: StateStatusNew, Kind: KindLabel, }, New: DiffLabelValues{ Name: "label-1", Color: "#FFFFFF", Description: "label 1 description", }, } assert.Contains(t, labels, expected) expected.MetaName = "label-2" expected.New.Name = "label-2" expected.New.Color = "#000000" expected.New.Description = "label 2 description" assert.Contains(t, labels, expected) }) }) t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/label.yml", kinds: []Kind{KindLabel}, skipResources: []ActionSkipResource{ { Kind: KindLabel, MetaName: "label-1", }, { Kind: KindLabel, MetaName: "label-2", }, { Kind: KindLabel, MetaName: "label-3", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Labels) }, }) }) }) t.Run("notification endpoints", func(t *testing.T) { t.Run("mixed update and created", func(t *testing.T) { testfileRunner(t, "testdata/notification_endpoint.yml", func(t *testing.T, template *Template) { fakeEndpointSVC := mock.NewNotificationEndpointService() id := platform.ID(1) existing := &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "http-none-auth-notification-endpoint", Description: "old desc", Status: taskmodel.TaskStatusInactive, }, Method: "POST", AuthMethod: "none", URL: "https://www.example.com/endpoint/old", } fakeEndpointSVC.FindNotificationEndpointsF = func(ctx context.Context, f influxdb.NotificationEndpointFilter, opt ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { return []influxdb.NotificationEndpoint{existing}, 1, nil } svc := newTestService(WithNotificationEndpointSVC(fakeEndpointSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) require.Len(t, impact.Diff.NotificationEndpoints, 5) var ( newEndpoints []DiffNotificationEndpoint existingEndpoints []DiffNotificationEndpoint ) for _, e := range impact.Diff.NotificationEndpoints { if e.Old != nil { existingEndpoints = append(existingEndpoints, e) continue } newEndpoints = append(newEndpoints, e) } require.Len(t, newEndpoints, 4) require.Len(t, existingEndpoints, 1) expected := DiffNotificationEndpoint{ DiffIdentifier: DiffIdentifier{ ID: 1, MetaName: "http-none-auth-notification-endpoint", StateStatus: StateStatusExists, Kind: KindNotificationEndpointHTTP, }, Old: &DiffNotificationEndpointValues{ NotificationEndpoint: existing, }, New: DiffNotificationEndpointValues{ NotificationEndpoint: &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "http-none-auth-notification-endpoint", Description: "http none auth desc", Status: taskmodel.TaskStatusActive, }, AuthMethod: "none", Method: "GET", URL: "https://www.example.com/endpoint/noneauth", }, }, } assert.Equal(t, expected, existingEndpoints[0]) }) }) t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/notification_endpoint.yml", kinds: []Kind{ KindNotificationEndpoint, KindNotificationEndpointHTTP, KindNotificationEndpointPagerDuty, KindNotificationEndpointSlack, }, skipResources: []ActionSkipResource{ { Kind: KindNotificationEndpoint, MetaName: "http-none-auth-notification-endpoint", }, { Kind: KindNotificationEndpoint, MetaName: "http-bearer-auth-notification-endpoint", }, { Kind: KindNotificationEndpointHTTP, MetaName: "http-basic-auth-notification-endpoint", }, { Kind: KindNotificationEndpointSlack, MetaName: "slack-notification-endpoint", }, { Kind: KindNotificationEndpointPagerDuty, MetaName: "pager-duty-notification-endpoint", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.NotificationEndpoints) }, }) }) }) t.Run("notification rules", func(t *testing.T) { t.Run("mixed update and created", func(t *testing.T) { testfileRunner(t, "testdata/notification_rule.yml", func(t *testing.T, template *Template) { fakeEndpointSVC := mock.NewNotificationEndpointService() id := platform.ID(1) existing := &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, // This name here matches the endpoint identified in the template notification rule Name: "endpoint-0", Description: "old desc", Status: taskmodel.TaskStatusInactive, }, Method: "POST", AuthMethod: "none", URL: "https://www.example.com/endpoint/old", } fakeEndpointSVC.FindNotificationEndpointsF = func(ctx context.Context, f influxdb.NotificationEndpointFilter, opt ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { return []influxdb.NotificationEndpoint{existing}, 1, nil } svc := newTestService(WithNotificationEndpointSVC(fakeEndpointSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) require.Len(t, impact.Diff.NotificationRules, 1) actual := impact.Diff.NotificationRules[0].New assert.Equal(t, "rule_0", actual.Name) assert.Equal(t, "desc_0", actual.Description) assert.Equal(t, "slack", actual.EndpointType) assert.Equal(t, existing.Name, actual.EndpointName) assert.Equal(t, SafeID(*existing.ID), actual.EndpointID) assert.Equal(t, (10 * time.Minute).String(), actual.Every) assert.Equal(t, (30 * time.Second).String(), actual.Offset) expectedStatusRules := []SummaryStatusRule{ {CurrentLevel: "CRIT", PreviousLevel: "OK"}, {CurrentLevel: "WARN"}, } assert.Equal(t, expectedStatusRules, actual.StatusRules) expectedTagRules := []SummaryTagRule{ {Key: "k1", Value: "v1", Operator: "equal"}, {Key: "k1", Value: "v2", Operator: "equal"}, } assert.Equal(t, expectedTagRules, actual.TagRules) }) }) t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/notification_rule.yml", kinds: []Kind{KindNotificationRule}, skipResources: []ActionSkipResource{ { Kind: KindNotificationRule, MetaName: "rule-uuid", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.NotificationRules) }, }) }) }) t.Run("secrets not returns missing secrets", func(t *testing.T) { testfileRunner(t, "testdata/notification_endpoint_secrets.yml", func(t *testing.T, template *Template) { fakeSecretSVC := mock.NewSecretService() fakeSecretSVC.GetSecretKeysFn = func(ctx context.Context, orgID platform.ID) ([]string, error) { return []string{"rando-1", "rando-2"}, nil } svc := newTestService(WithSecretSVC(fakeSecretSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) assert.Equal(t, []string{"routing-key"}, impact.Summary.MissingSecrets) }) }) t.Run("tasks", func(t *testing.T) { t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/tasks.yml", kinds: []Kind{KindTask}, skipResources: []ActionSkipResource{ { Kind: KindTask, MetaName: "task-uuid", }, { Kind: KindTask, MetaName: "task-1", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Tasks) }, }) }) }) t.Run("telegraf configs", func(t *testing.T) { t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/telegraf.yml", kinds: []Kind{KindTelegraf}, skipResources: []ActionSkipResource{ { Kind: KindTelegraf, MetaName: "first-tele-config", }, { Kind: KindTelegraf, MetaName: "tele-2", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Telegrafs) }, }) }) }) t.Run("variables", func(t *testing.T) { t.Run("mixed update and created", func(t *testing.T) { testfileRunner(t, "testdata/variables.json", func(t *testing.T, template *Template) { fakeVarSVC := mock.NewVariableService() fakeVarSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, opts ...influxdb.FindOptions) ([]*influxdb.Variable, error) { return []*influxdb.Variable{ { ID: platform.ID(1), Name: "var-const-3", Description: "old desc", }, }, nil } svc := newTestService(WithVariableSVC(fakeVarSVC)) impact, err := svc.DryRun(context.TODO(), platform.ID(100), 0, ApplyWithTemplate(template)) require.NoError(t, err) variables := impact.Diff.Variables require.Len(t, variables, 4) expected := DiffVariable{ DiffIdentifier: DiffIdentifier{ ID: 1, MetaName: "var-const-3", StateStatus: StateStatusExists, Kind: KindVariable, }, Old: &DiffVariableValues{ Name: "var-const-3", Description: "old desc", }, New: DiffVariableValues{ Name: "var-const-3", Description: "var-const-3 desc", Args: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"first val"}, }, }, } assert.Equal(t, expected, variables[0]) expected = DiffVariable{ DiffIdentifier: DiffIdentifier{ // no ID here since this one would be new MetaName: "var-map-4", StateStatus: StateStatusNew, Kind: KindVariable, }, New: DiffVariableValues{ Name: "var-map-4", Description: "var-map-4 desc", Args: &influxdb.VariableArguments{ Type: "map", Values: influxdb.VariableMapValues{"k1": "v1"}, }, }, } assert.Equal(t, expected, variables[1]) }) }) t.Run("with actions applied", func(t *testing.T) { testDryRunActions(t, dryRunTestFields{ path: "testdata/variables.yml", kinds: []Kind{KindVariable}, skipResources: []ActionSkipResource{ { Kind: KindVariable, MetaName: "var-query-1", }, { Kind: KindVariable, MetaName: "var-query-2", }, { Kind: KindVariable, MetaName: "var-const-3", }, { Kind: KindVariable, MetaName: "var-map-4", }, }, assertFn: func(t *testing.T, impact ImpactSummary) { require.Empty(t, impact.Diff.Variables) }, }) }) }) }) t.Run("Apply", func(t *testing.T) { t.Run("buckets", func(t *testing.T) { t.Run("successfully creates template of buckets", func(t *testing.T) { testfileRunner(t, "testdata/bucket.yml", func(t *testing.T, template *Template) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error { b.ID = platform.ID(b.RetentionPeriod) return nil } fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id platform.ID, s string) (*influxdb.Bucket, error) { // forces the bucket to be created a new return nil, errors.New("an error") } fakeBktSVC.UpdateBucketFn = func(_ context.Context, id platform.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) { return &influxdb.Bucket{ID: id}, nil } svc := newTestService(WithBucketSVC(fakeBktSVC)) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Buckets, 2) expected := SummaryBucket{ SummaryIdentifier: SummaryIdentifier{ Kind: KindBucket, MetaName: "rucket-11", EnvReferences: []SummaryReference{}, }, ID: SafeID(time.Hour), OrgID: SafeID(orgID), Name: "rucket-11", Description: "bucket 1 description", RetentionPeriod: time.Hour, LabelAssociations: []SummaryLabel{}, } assert.Contains(t, sum.Buckets, expected) }) }) t.Run("will not apply bucket if no changes to be applied", func(t *testing.T) { testfileRunner(t, "testdata/bucket.yml", func(t *testing.T, template *Template) { orgID := platform.ID(9000) fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(ctx context.Context, oid platform.ID, name string) (*influxdb.Bucket, error) { if orgID != oid { return nil, errors.New("invalid org id") } id := platform.ID(3) if name == "display name" { id = 4 name = "rucket-22" } if bkt, ok := template.mBuckets[name]; ok { return &influxdb.Bucket{ ID: id, OrgID: oid, Name: bkt.Name(), Description: bkt.Description, RetentionPeriod: bkt.RetentionRules.RP(), }, nil } return nil, errors.New("not found") } fakeBktSVC.UpdateBucketFn = func(_ context.Context, id platform.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) { return &influxdb.Bucket{ID: id}, nil } svc := newTestService(WithBucketSVC(fakeBktSVC)) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Buckets, 2) expected := SummaryBucket{ SummaryIdentifier: SummaryIdentifier{ Kind: KindBucket, MetaName: "rucket-11", EnvReferences: []SummaryReference{}, }, ID: SafeID(3), OrgID: SafeID(orgID), Name: "rucket-11", Description: "bucket 1 description", RetentionPeriod: time.Hour, LabelAssociations: []SummaryLabel{}, } assert.Contains(t, sum.Buckets, expected) assert.Zero(t, fakeBktSVC.CreateBucketCalls.Count()) assert.Zero(t, fakeBktSVC.UpdateBucketCalls.Count()) }) }) t.Run("rolls back all created buckets on an error", func(t *testing.T) { testfileRunner(t, "testdata/bucket.yml", func(t *testing.T, template *Template) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id platform.ID, s string) (*influxdb.Bucket, error) { // forces the bucket to be created a new return nil, errors.New("an error") } fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error { if fakeBktSVC.CreateBucketCalls.Count() == 1 { return errors.New("blowed up ") } return nil } svc := newTestService(WithBucketSVC(fakeBktSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.GreaterOrEqual(t, fakeBktSVC.DeleteBucketCalls.Count(), 1) }) }) }) t.Run("checks", func(t *testing.T) { t.Run("successfully creates template of checks", func(t *testing.T) { testfileRunner(t, "testdata/checks.yml", func(t *testing.T, template *Template) { fakeCheckSVC := mock.NewCheckService() fakeCheckSVC.CreateCheckFn = func(ctx context.Context, c influxdb.CheckCreate, id platform.ID) error { c.SetID(platform.ID(fakeCheckSVC.CreateCheckCalls.Count() + 1)) return nil } svc := newTestService(WithCheckSVC(fakeCheckSVC)) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Checks, 2) containsWithID := func(t *testing.T, name string) { t.Helper() for _, actualNotification := range sum.Checks { actual := actualNotification.Check if actual.GetID() == 0 { assert.NotZero(t, actual.GetID()) } if actual.GetName() == name { return } } assert.Fail(t, "did not find notification by name: "+name) } for _, expectedName := range []string{"check-0", "display name"} { containsWithID(t, expectedName) } }) }) t.Run("rolls back all created checks on an error", func(t *testing.T) { testfileRunner(t, "testdata/checks.yml", func(t *testing.T, template *Template) { fakeCheckSVC := mock.NewCheckService() fakeCheckSVC.CreateCheckFn = func(ctx context.Context, c influxdb.CheckCreate, id platform.ID) error { c.SetID(platform.ID(fakeCheckSVC.CreateCheckCalls.Count() + 1)) if fakeCheckSVC.CreateCheckCalls.Count() == 1 { return errors.New("hit that kill count") } return nil } svc := newTestService(WithCheckSVC(fakeCheckSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.GreaterOrEqual(t, fakeCheckSVC.DeleteCheckCalls.Count(), 1) }) }) }) t.Run("labels", func(t *testing.T) { t.Run("successfully creates template of labels", func(t *testing.T) { testfileRunner(t, "testdata/label.json", func(t *testing.T, template *Template) { fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { i, err := strconv.Atoi(l.Name[len(l.Name)-1:]) if err != nil { return nil } l.ID = platform.ID(i) return nil } svc := newTestService(WithLabelSVC(fakeLabelSVC)) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Labels, 3) expectedLabel := sumLabelGen("label-1", "label-1", "#FFFFFF", "label 1 description") expectedLabel.ID = 1 expectedLabel.OrgID = SafeID(orgID) assert.Contains(t, sum.Labels, expectedLabel) expectedLabel = sumLabelGen("label-2", "label-2", "#000000", "label 2 description") expectedLabel.ID = 2 expectedLabel.OrgID = SafeID(orgID) assert.Contains(t, sum.Labels, expectedLabel) }) }) t.Run("rolls back all created labels on an error", func(t *testing.T) { testfileRunner(t, "testdata/label", func(t *testing.T, template *Template) { fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { // 3rd/4th label will return the error here, and 2 before should be rolled back if fakeLabelSVC.CreateLabelCalls.Count() == 2 { return errors.New("blowed up ") } return nil } svc := newTestService(WithLabelSVC(fakeLabelSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.GreaterOrEqual(t, fakeLabelSVC.DeleteLabelCalls.Count(), 1) }) }) t.Run("will not apply label if no changes to be applied", func(t *testing.T) { testfileRunner(t, "testdata/label.yml", func(t *testing.T, template *Template) { orgID := platform.ID(9000) stubExisting := func(name string, id platform.ID) *influxdb.Label { templateLabel := template.mLabels[name] return &influxdb.Label{ // makes all template changes same as they are on the existing ID: id, OrgID: orgID, Name: templateLabel.Name(), Properties: map[string]string{ "color": templateLabel.Color, "description": templateLabel.Description, }, } } stubExisting("label-1", 1) stubExisting("label-3", 3) fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.FindLabelsFn = func(ctx context.Context, f influxdb.LabelFilter) ([]*influxdb.Label, error) { if f.Name != "label-1" && f.Name != "display name" { return nil, nil } id := platform.ID(1) name := f.Name if f.Name == "display name" { id = 3 name = "label-3" } return []*influxdb.Label{stubExisting(name, id)}, nil } fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { if l.Name == "label-2" { l.ID = 2 } return nil } fakeLabelSVC.UpdateLabelFn = func(_ context.Context, id platform.ID, l influxdb.LabelUpdate) (*influxdb.Label, error) { if id == platform.ID(3) { return nil, errors.New("invalid id provided") } return &influxdb.Label{ID: id}, nil } svc := newTestService(WithLabelSVC(fakeLabelSVC)) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Labels, 3) expectedLabel := sumLabelGen("label-1", "label-1", "#FFFFFF", "label 1 description") expectedLabel.ID = 1 expectedLabel.OrgID = SafeID(orgID) assert.Contains(t, sum.Labels, expectedLabel) expectedLabel = sumLabelGen("label-2", "label-2", "#000000", "label 2 description") expectedLabel.ID = 2 expectedLabel.OrgID = SafeID(orgID) assert.Contains(t, sum.Labels, expectedLabel) assert.Equal(t, 1, fakeLabelSVC.CreateLabelCalls.Count()) // only called for second label }) }) }) t.Run("dashboards", func(t *testing.T) { t.Run("successfully creates a dashboard", func(t *testing.T) { testfileRunner(t, "testdata/dashboard.yml", func(t *testing.T, template *Template) { fakeDashSVC := mock.NewDashboardService() fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error { d.ID = platform.ID(1) return nil } fakeDashSVC.UpdateDashboardCellViewF = func(ctx context.Context, dID platform.ID, cID platform.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) { return &influxdb.View{}, nil } svc := newTestService(WithDashboardSVC(fakeDashSVC)) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Dashboards, 2) dash1 := sum.Dashboards[0] assert.NotZero(t, dash1.ID) assert.NotZero(t, dash1.OrgID) assert.Equal(t, "dash-1", dash1.MetaName) assert.Equal(t, "display name", dash1.Name) require.Len(t, dash1.Charts, 1) dash2 := sum.Dashboards[1] assert.NotZero(t, dash2.ID) assert.Equal(t, "dash-2", dash2.MetaName) assert.Equal(t, "dash-2", dash2.Name) require.Empty(t, dash2.Charts) }) }) t.Run("rolls back created dashboard on an error", func(t *testing.T) { testfileRunner(t, "testdata/dashboard.yml", func(t *testing.T, template *Template) { fakeDashSVC := mock.NewDashboardService() fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error { // error out on second dashboard attempted if fakeDashSVC.CreateDashboardCalls.Count() == 1 { return errors.New("blowed up ") } d.ID = platform.ID(1) return nil } deletedDashs := make(map[platform.ID]bool) fakeDashSVC.DeleteDashboardF = func(_ context.Context, id platform.ID) error { deletedDashs[id] = true return nil } svc := newTestService(WithDashboardSVC(fakeDashSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.True(t, deletedDashs[1]) }) }) }) t.Run("label mapping", func(t *testing.T) { testLabelMappingApplyFn := func(t *testing.T, filename string, numExpected int, settersFn func() []ServiceSetterFn) { t.Helper() testfileRunner(t, filename, func(t *testing.T, template *Template) { t.Helper() fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { l.ID = platform.ID(rand.Int()) return nil } fakeLabelSVC.CreateLabelMappingFn = func(_ context.Context, mapping *influxdb.LabelMapping) error { if mapping.ResourceID == 0 { return errors.New("did not get a resource ID") } if mapping.ResourceType == "" { return errors.New("did not get a resource type") } return nil } svc := newTestService(append(settersFn(), WithLabelSVC(fakeLabelSVC), WithLogger(zaptest.NewLogger(t)), )...) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) assert.Equal(t, numExpected, fakeLabelSVC.CreateLabelMappingCalls.Count()) }) } testLabelMappingRollbackFn := func(t *testing.T, filename string, killCount int, settersFn func() []ServiceSetterFn) { t.Helper() testfileRunner(t, filename, func(t *testing.T, template *Template) { t.Helper() fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { l.ID = platform.ID(fakeLabelSVC.CreateLabelCalls.Count() + 1) return nil } fakeLabelSVC.CreateLabelMappingFn = func(_ context.Context, mapping *influxdb.LabelMapping) error { if mapping.ResourceID == 0 { return errors.New("did not get a resource ID") } if mapping.ResourceType == "" { return errors.New("did not get a resource type") } if fakeLabelSVC.CreateLabelMappingCalls.Count() == killCount { return errors.New("hit last label") } return nil } svc := newTestService(append(settersFn(), WithLabelSVC(fakeLabelSVC), WithLogger(zaptest.NewLogger(t)), )...) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.GreaterOrEqual(t, fakeLabelSVC.DeleteLabelMappingCalls.Count(), killCount) }) } t.Run("maps buckets with labels", func(t *testing.T) { bktOpt := func() []ServiceSetterFn { fakeBktSVC := mock.NewBucketService() fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error { b.ID = platform.ID(rand.Int()) return nil } fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id platform.ID, s string) (*influxdb.Bucket, error) { // forces the bucket to be created a new return nil, errors.New("an error") } return []ServiceSetterFn{WithBucketSVC(fakeBktSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/bucket_associates_label.yml", 4, bktOpt) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/bucket_associates_label.yml", 2, bktOpt) }) }) t.Run("maps checks with labels", func(t *testing.T) { opts := func() []ServiceSetterFn { fakeCheckSVC := mock.NewCheckService() fakeCheckSVC.CreateCheckFn = func(ctx context.Context, c influxdb.CheckCreate, id platform.ID) error { c.Check.SetID(platform.ID(rand.Int())) return nil } fakeCheckSVC.FindCheckFn = func(ctx context.Context, f influxdb.CheckFilter) (influxdb.Check, error) { return nil, errors.New("check not found") } return []ServiceSetterFn{WithCheckSVC(fakeCheckSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/checks.yml", 2, opts) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/checks.yml", 1, opts) }) }) t.Run("maps dashboards with labels", func(t *testing.T) { opts := func() []ServiceSetterFn { fakeDashSVC := mock.NewDashboardService() fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error { d.ID = platform.ID(rand.Int()) return nil } return []ServiceSetterFn{WithDashboardSVC(fakeDashSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/dashboard_associates_label.yml", 2, opts) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/dashboard_associates_label.yml", 1, opts) }) }) t.Run("maps notification endpoints with labels", func(t *testing.T) { opts := func() []ServiceSetterFn { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID platform.ID) error { nr.SetID(platform.ID(rand.Int())) return nil } return []ServiceSetterFn{WithNotificationEndpointSVC(fakeEndpointSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/notification_endpoint.yml", 5, opts) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/notification_endpoint.yml", 3, opts) }) }) t.Run("maps notification rules with labels", func(t *testing.T) { opts := func() []ServiceSetterFn { fakeRuleStore := mock.NewNotificationRuleStore() fakeRuleStore.CreateNotificationRuleF = func(ctx context.Context, nr influxdb.NotificationRuleCreate, userID platform.ID) error { nr.SetID(platform.ID(fakeRuleStore.CreateNotificationRuleCalls.Count() + 1)) return nil } return []ServiceSetterFn{ WithNotificationRuleSVC(fakeRuleStore), } } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/notification_rule.yml", 2, opts) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/notification_rule.yml", 1, opts) }) }) t.Run("maps tasks with labels", func(t *testing.T) { opts := func() []ServiceSetterFn { fakeTaskSVC := mock.NewTaskService() fakeTaskSVC.CreateTaskFn = func(ctx context.Context, tc taskmodel.TaskCreate) (*taskmodel.Task, error) { reg := regexp.MustCompile(`name: "(.+)",`) names := reg.FindStringSubmatch(tc.Flux) if len(names) < 2 { return nil, errors.New("bad flux query provided: " + tc.Flux) } return &taskmodel.Task{ ID: platform.ID(rand.Int()), Type: tc.Type, OrganizationID: tc.OrganizationID, OwnerID: tc.OwnerID, Name: names[1], Description: tc.Description, Status: tc.Status, Flux: tc.Flux, }, nil } return []ServiceSetterFn{WithTaskSVC(fakeTaskSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/tasks.yml", 2, opts) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/tasks.yml", 1, opts) }) }) t.Run("maps telegrafs with labels", func(t *testing.T) { opts := func() []ServiceSetterFn { fakeTeleSVC := mock.NewTelegrafConfigStore() fakeTeleSVC.CreateTelegrafConfigF = func(_ context.Context, cfg *influxdb.TelegrafConfig, _ platform.ID) error { cfg.ID = platform.ID(rand.Int()) return nil } return []ServiceSetterFn{WithTelegrafSVC(fakeTeleSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/telegraf.yml", 2, opts) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/telegraf.yml", 1, opts) }) }) t.Run("maps variables with labels", func(t *testing.T) { opt := func() []ServiceSetterFn { fakeVarSVC := mock.NewVariableService() fakeVarSVC.CreateVariableF = func(_ context.Context, v *influxdb.Variable) error { v.ID = platform.ID(rand.Int()) return nil } return []ServiceSetterFn{WithVariableSVC(fakeVarSVC)} } t.Run("applies successfully", func(t *testing.T) { testLabelMappingApplyFn(t, "testdata/variable_associates_label.yml", 1, opt) }) t.Run("deletes new label mappings on error", func(t *testing.T) { testLabelMappingRollbackFn(t, "testdata/variable_associates_label.yml", 0, opt) }) }) }) t.Run("notification endpoints", func(t *testing.T) { t.Run("successfully creates template of endpoints", func(t *testing.T) { testfileRunner(t, "testdata/notification_endpoint.yml", func(t *testing.T, template *Template) { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID platform.ID) error { nr.SetID(platform.ID(fakeEndpointSVC.CreateNotificationEndpointCalls.Count() + 1)) return nil } svc := newTestService(WithNotificationEndpointSVC(fakeEndpointSVC)) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.NotificationEndpoints, 5) containsWithID := func(t *testing.T, name string) { var endpoints []string for _, actualNotification := range sum.NotificationEndpoints { actual := actualNotification.NotificationEndpoint if actual.GetID() == 0 { assert.NotZero(t, actual.GetID()) } if actual.GetName() == name { return } endpoints = append(endpoints, fmt.Sprintf("%+v", actual)) } assert.Failf(t, "did not find notification by name: "+name, "endpoints received: %s", endpoints) } expectedNames := []string{ "basic endpoint name", "http-bearer-auth-notification-endpoint", "http-none-auth-notification-endpoint", "pager duty name", "slack name", } for _, expectedName := range expectedNames { containsWithID(t, expectedName) } }) }) t.Run("rolls back all created notifications on an error", func(t *testing.T) { testfileRunner(t, "testdata/notification_endpoint.yml", func(t *testing.T, template *Template) { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID platform.ID) error { nr.SetID(platform.ID(fakeEndpointSVC.CreateNotificationEndpointCalls.Count() + 1)) if fakeEndpointSVC.CreateNotificationEndpointCalls.Count() == 3 { return errors.New("hit that kill count") } return nil } svc := newTestService(WithNotificationEndpointSVC(fakeEndpointSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.GreaterOrEqual(t, fakeEndpointSVC.DeleteNotificationEndpointCalls.Count(), 3) }) }) }) t.Run("notification rules", func(t *testing.T) { t.Run("successfully creates", func(t *testing.T) { testfileRunner(t, "testdata/notification_rule.yml", func(t *testing.T, template *Template) { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID platform.ID) error { nr.SetID(platform.ID(fakeEndpointSVC.CreateNotificationEndpointCalls.Count() + 1)) return nil } fakeRuleStore := mock.NewNotificationRuleStore() fakeRuleStore.CreateNotificationRuleF = func(ctx context.Context, nr influxdb.NotificationRuleCreate, userID platform.ID) error { nr.SetID(platform.ID(fakeRuleStore.CreateNotificationRuleCalls.Count() + 1)) return nil } svc := newTestService( WithNotificationEndpointSVC(fakeEndpointSVC), WithNotificationRuleSVC(fakeRuleStore), ) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.NotificationRules, 1) assert.Equal(t, "rule-uuid", sum.NotificationRules[0].MetaName) assert.Equal(t, "rule_0", sum.NotificationRules[0].Name) assert.Equal(t, "desc_0", sum.NotificationRules[0].Description) assert.Equal(t, SafeID(1), sum.NotificationRules[0].EndpointID) assert.Equal(t, "endpoint-0", sum.NotificationRules[0].EndpointMetaName) assert.Equal(t, "slack", sum.NotificationRules[0].EndpointType) }) }) t.Run("rolls back all created notification rules on an error", func(t *testing.T) { testfileRunner(t, "testdata/notification_rule.yml", func(t *testing.T, template *Template) { fakeRuleStore := mock.NewNotificationRuleStore() fakeRuleStore.CreateNotificationRuleF = func(ctx context.Context, nr influxdb.NotificationRuleCreate, userID platform.ID) error { nr.SetID(platform.ID(fakeRuleStore.CreateNotificationRuleCalls.Count() + 1)) return nil } fakeRuleStore.DeleteNotificationRuleF = func(ctx context.Context, id platform.ID) error { if id != 1 { return errors.New("wrong id here") } return nil } fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(ctx context.Context, l *influxdb.Label) error { l.ID = platform.ID(fakeLabelSVC.CreateLabelCalls.Count() + 1) return nil } fakeLabelSVC.CreateLabelMappingFn = func(ctx context.Context, m *influxdb.LabelMapping) error { return errors.New("start the rollack") } svc := newTestService( WithLabelSVC(fakeLabelSVC), WithNotificationRuleSVC(fakeRuleStore), ) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.Equal(t, 1, fakeRuleStore.DeleteNotificationRuleCalls.Count()) }) }) }) t.Run("tasks", func(t *testing.T) { t.Run("successfuly creates", func(t *testing.T) { testfileRunner(t, "testdata/tasks.yml", func(t *testing.T, template *Template) { orgID := platform.ID(9000) fakeTaskSVC := mock.NewTaskService() fakeTaskSVC.CreateTaskFn = func(ctx context.Context, tc taskmodel.TaskCreate) (*taskmodel.Task, error) { reg := regexp.MustCompile(`name: "(.+)",`) names := reg.FindStringSubmatch(tc.Flux) if len(names) < 2 { return nil, errors.New("bad flux query provided: " + tc.Flux) } return &taskmodel.Task{ ID: platform.ID(fakeTaskSVC.CreateTaskCalls.Count() + 1), Type: tc.Type, OrganizationID: tc.OrganizationID, OwnerID: tc.OwnerID, Name: names[1], Description: tc.Description, Status: tc.Status, Flux: tc.Flux, }, nil } svc := newTestService(WithTaskSVC(fakeTaskSVC)) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Tasks, 2) assert.NotZero(t, sum.Tasks[0].ID) assert.Equal(t, "task-1", sum.Tasks[0].MetaName) assert.Equal(t, "task-1", sum.Tasks[0].Name) assert.Equal(t, "desc_1", sum.Tasks[0].Description) assert.NotZero(t, sum.Tasks[1].ID) assert.Equal(t, "task-uuid", sum.Tasks[1].MetaName) assert.Equal(t, "task-0", sum.Tasks[1].Name) assert.Equal(t, "desc_0", sum.Tasks[1].Description) }) }) t.Run("rolls back all created tasks on an error", func(t *testing.T) { testfileRunner(t, "testdata/tasks.yml", func(t *testing.T, template *Template) { fakeTaskSVC := mock.NewTaskService() fakeTaskSVC.CreateTaskFn = func(ctx context.Context, tc taskmodel.TaskCreate) (*taskmodel.Task, error) { if fakeTaskSVC.CreateTaskCalls.Count() == 1 { return nil, errors.New("expected error") } return &taskmodel.Task{ ID: platform.ID(fakeTaskSVC.CreateTaskCalls.Count() + 1), }, nil } svc := newTestService(WithTaskSVC(fakeTaskSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.Equal(t, 1, fakeTaskSVC.DeleteTaskCalls.Count()) }) }) }) t.Run("telegrafs", func(t *testing.T) { t.Run("successfuly creates", func(t *testing.T) { testfileRunner(t, "testdata/telegraf.yml", func(t *testing.T, template *Template) { orgID := platform.ID(9000) fakeTeleSVC := mock.NewTelegrafConfigStore() fakeTeleSVC.CreateTelegrafConfigF = func(_ context.Context, tc *influxdb.TelegrafConfig, userID platform.ID) error { tc.ID = 1 return nil } svc := newTestService(WithTelegrafSVC(fakeTeleSVC)) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.TelegrafConfigs, 2) assert.Equal(t, "display name", sum.TelegrafConfigs[0].TelegrafConfig.Name) assert.Equal(t, "desc", sum.TelegrafConfigs[0].TelegrafConfig.Description) assert.Equal(t, "tele-2", sum.TelegrafConfigs[1].TelegrafConfig.Name) }) }) t.Run("rolls back all created telegrafs on an error", func(t *testing.T) { testfileRunner(t, "testdata/telegraf.yml", func(t *testing.T, template *Template) { fakeTeleSVC := mock.NewTelegrafConfigStore() fakeTeleSVC.CreateTelegrafConfigF = func(_ context.Context, tc *influxdb.TelegrafConfig, userID platform.ID) error { t.Log("called") if fakeTeleSVC.CreateTelegrafConfigCalls.Count() == 1 { return errors.New("limit hit") } tc.ID = platform.ID(1) return nil } fakeTeleSVC.DeleteTelegrafConfigF = func(_ context.Context, id platform.ID) error { if id != 1 { return errors.New("wrong id here") } return nil } svc := newTestService(WithTelegrafSVC(fakeTeleSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.Equal(t, 1, fakeTeleSVC.DeleteTelegrafConfigCalls.Count()) }) }) }) t.Run("variables", func(t *testing.T) { t.Run("successfully creates template of variables", func(t *testing.T) { testfileRunner(t, "testdata/variables.yml", func(t *testing.T, template *Template) { fakeVarSVC := mock.NewVariableService() fakeVarSVC.CreateVariableF = func(_ context.Context, v *influxdb.Variable) error { v.ID = platform.ID(fakeVarSVC.CreateVariableCalls.Count() + 1) return nil } svc := newTestService(WithVariableSVC(fakeVarSVC)) orgID := platform.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Variables, 4) actual := sum.Variables[0] assert.True(t, actual.ID > 0 && actual.ID < 5) assert.Equal(t, SafeID(orgID), actual.OrgID) assert.Equal(t, "var-const-3", actual.Name) assert.Equal(t, "var-const-3 desc", actual.Description) require.NotNil(t, actual.Arguments) assert.Equal(t, influxdb.VariableConstantValues{"first val"}, actual.Arguments.Values) actual = sum.Variables[2] assert.Equal(t, []string{"rucket"}, actual.Selected) for _, actual := range sum.Variables { assert.Containsf(t, []SafeID{1, 2, 3, 4}, actual.ID, "actual var: %+v", actual) } }) }) t.Run("rolls back all created variables on an error", func(t *testing.T) { testfileRunner(t, "testdata/variables.yml", func(t *testing.T, template *Template) { fakeVarSVC := mock.NewVariableService() fakeVarSVC.CreateVariableF = func(_ context.Context, l *influxdb.Variable) error { // 4th variable will return the error here, and 3 before should be rolled back if fakeVarSVC.CreateVariableCalls.Count() == 2 { return errors.New("blowed up ") } return nil } svc := newTestService(WithVariableSVC(fakeVarSVC)) orgID := platform.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.Error(t, err) assert.GreaterOrEqual(t, fakeVarSVC.DeleteVariableCalls.Count(), 1) }) }) t.Run("will not apply variable if no changes to be applied", func(t *testing.T) { testfileRunner(t, "testdata/variables.yml", func(t *testing.T, template *Template) { orgID := platform.ID(9000) fakeVarSVC := mock.NewVariableService() fakeVarSVC.FindVariablesF = func(ctx context.Context, f influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { return []*influxdb.Variable{ { // makes all template changes same as they are on the existing ID: platform.ID(1), OrganizationID: orgID, Name: template.mVariables["var-const-3"].Name(), Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"first val"}, }, }, }, nil } fakeVarSVC.CreateVariableF = func(_ context.Context, l *influxdb.Variable) error { if l.Name == "var_const" { return errors.New("shouldn't get here") } return nil } fakeVarSVC.UpdateVariableF = func(_ context.Context, id platform.ID, v *influxdb.VariableUpdate) (*influxdb.Variable, error) { if id > platform.ID(1) { return nil, errors.New("this id should not be updated") } return &influxdb.Variable{ID: id}, nil } svc := newTestService(WithVariableSVC(fakeVarSVC)) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithTemplate(template)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Variables, 4) expected := sum.Variables[0] assert.Equal(t, SafeID(1), expected.ID) assert.Equal(t, "var-const-3", expected.Name) assert.Equal(t, 3, fakeVarSVC.CreateVariableCalls.Count()) // only called for last 3 labels }) }) }) }) t.Run("Export", func(t *testing.T) { newThresholdBase := func(i int) icheck.Base { return icheck.Base{ ID: platform.ID(i), TaskID: 300, Name: fmt.Sprintf("check_%d", i), Description: fmt.Sprintf("desc_%d", i), Every: mustDuration(t, time.Minute), Offset: mustDuration(t, 15*time.Second), Query: influxdb.DashboardQuery{ Text: `from(bucket: "telegraf") |> range(start: -1m) |> filter(fn: (r) => r._field == "usage_user")`, }, StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }", Tags: []influxdb.Tag{ {Key: "key_1", Value: "val_1"}, {Key: "key_2", Value: "val_2"}, }, } } sortLabelsByName := func(labels []SummaryLabel) { sort.Slice(labels, func(i, j int) bool { return labels[i].Name < labels[j].Name }) } t.Run("with existing resources", func(t *testing.T) { encodeAndDecode := func(t *testing.T, template *Template) *Template { t.Helper() b, err := template.Encode(EncodingJSON) require.NoError(t, err) newTemplate, err := Parse(EncodingJSON, FromReader(bytes.NewReader(b))) require.NoError(t, err) return newTemplate } t.Run("bucket", func(t *testing.T) { tests := []struct { name string newName string }{ { name: "without new name", }, { name: "with new name", newName: "new name", }, } for _, tt := range tests { fn := func(t *testing.T) { expected := &influxdb.Bucket{ ID: 3, Name: "bucket name", Description: "desc", RetentionPeriod: time.Hour, } bktSVC := mock.NewBucketService() bktSVC.FindBucketByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Bucket, error) { if id != expected.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expected, nil } bktSVC.FindBucketsFn = func(_ context.Context, filter influxdb.BucketFilter, _ ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { if filter.ID != nil { if *filter.ID != expected.ID { return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) } } else if filter.Name != nil && *filter.Name != expected.Name { return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) } return []*influxdb.Bucket{expected}, 1, nil } svc := newTestService(WithBucketSVC(bktSVC), WithLabelSVC(mock.NewLabelService())) resToClone := ResourceToClone{ Kind: KindBucket, ID: expected.ID, Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) bkts := newTemplate.Summary().Buckets require.Len(t, bkts, 1) actual := bkts[0] expectedName := expected.Name if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actual.Name) assert.Equal(t, expected.Description, actual.Description) assert.Equal(t, expected.RetentionPeriod, actual.RetentionPeriod) } t.Run(tt.name, fn) } }) // todo: bucket names are unique. t.Run("bucket by name", func(t *testing.T) { knownBuckets := []*influxdb.Bucket{ { ID: platform.ID(1), Name: "bucket", Description: "desc", RetentionPeriod: time.Hour, }, { ID: platform.ID(2), Name: "bucketCopy", Description: "desc", RetentionPeriod: time.Hour, }, { ID: platform.ID(3), Name: "bucket3", Description: "desc", RetentionPeriod: time.Hour, }, } tests := []struct { name string findName string findID platform.ID expected []*influxdb.Bucket }{ { name: "find bucket with unique name", findName: "bucket", expected: []*influxdb.Bucket{knownBuckets[0]}, }, { name: "find no buckets", findName: "fakeBucket", expected: nil, }, { name: "find bucket by id", findID: platform.ID(2), expected: []*influxdb.Bucket{knownBuckets[1]}, }, { // todo: verify this is intended behavior (it is in swagger) name: "find by id, set new name", findID: platform.ID(2), findName: "renamedBucket", expected: []*influxdb.Bucket{knownBuckets[1]}, }, } for _, tt := range tests { fn := func(t *testing.T) { bktSVC := mock.NewBucketService() bktSVC.FindBucketsFn = func(_ context.Context, filter influxdb.BucketFilter, _ ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { if filter.ID != nil { for i := range knownBuckets { if knownBuckets[i].ID == *filter.ID { return []*influxdb.Bucket{knownBuckets[i]}, 1, nil } } return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) } else if filter.Name != nil { bkts := []*influxdb.Bucket{} for i := range knownBuckets { if knownBuckets[i].Name == *filter.Name { bkts = append(bkts, knownBuckets[i]) } } if lBkts := len(bkts); lBkts > 0 { return bkts, lBkts, nil } return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) } return knownBuckets, len(knownBuckets), nil } resToClone := ResourceToClone{ Kind: KindBucket, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService( WithBucketSVC(bktSVC), WithLabelSVC(mock.NewLabelService()), ) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].Name = tt.findName } actual := template.Summary().Buckets require.Len(t, actual, len(tt.expected)) for i := range actual { // can't verify id's match due to the use of SafeID's assert.Equal(t, tt.expected[i].Name, actual[i].Name) assert.Equal(t, tt.expected[i].Description, actual[i].Description) assert.Equal(t, tt.expected[i].RetentionPeriod, actual[i].RetentionPeriod) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("checks", func(t *testing.T) { tests := []struct { name string newName string expected influxdb.Check }{ { name: "threshold", expected: &icheck.Threshold{ Base: newThresholdBase(0), Thresholds: []icheck.ThresholdConfig{ icheck.Lesser{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Critical, }, Value: 20, }, icheck.Greater{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Warn, }, Value: 30, }, icheck.Range{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Info, }, Within: false, // outside_range Min: 10, Max: 25, }, icheck.Range{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Ok, }, Within: true, // inside_range Min: 21, Max: 24, }, }, }, }, { name: "deadman", newName: "new name", expected: &icheck.Deadman{ Base: newThresholdBase(1), TimeSince: mustDuration(t, time.Hour), StaleTime: mustDuration(t, 5*time.Hour), ReportZero: true, Level: notification.Critical, }, }, } for _, tt := range tests { fn := func(t *testing.T) { id := platform.ID(1) tt.expected.SetID(id) checkSVC := mock.NewCheckService() checkSVC.FindCheckByIDFn = func(ctx context.Context, id platform.ID) (influxdb.Check, error) { if id != tt.expected.GetID() { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return tt.expected, nil } checkSVC.FindChecksFn = func(_ context.Context, filter influxdb.CheckFilter, _ ...influxdb.FindOptions) ([]influxdb.Check, int, error) { if filter.ID != nil { if *filter.ID != tt.expected.GetID() { return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) } } else if filter.Name != nil && *filter.Name != tt.expected.GetName() { return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) } return []influxdb.Check{tt.expected}, 1, nil } svc := newTestService(WithCheckSVC(checkSVC)) resToClone := ResourceToClone{ Kind: KindCheck, ID: tt.expected.GetID(), Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) checks := newTemplate.Summary().Checks require.Len(t, checks, 1) actual := checks[0].Check expectedName := tt.expected.GetName() if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actual.GetName()) } t.Run(tt.name, fn) } }) t.Run("checks by name", func(t *testing.T) { knownChecks := []influxdb.Check{ &icheck.Threshold{ Base: newThresholdBase(0), Thresholds: []icheck.ThresholdConfig{ icheck.Lesser{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Critical, }, Value: 20, }, icheck.Greater{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Warn, }, Value: 30, }, icheck.Range{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Info, }, Within: false, // outside_range Min: 10, Max: 25, }, icheck.Range{ ThresholdConfigBase: icheck.ThresholdConfigBase{ AllValues: true, Level: notification.Ok, }, Within: true, // inside_range Min: 21, Max: 24, }, }, }, &icheck.Deadman{ Base: newThresholdBase(1), TimeSince: mustDuration(t, time.Hour), StaleTime: mustDuration(t, 5*time.Hour), ReportZero: true, Level: notification.Critical, }, &icheck.Deadman{ Base: icheck.Base{ ID: platform.ID(2), TaskID: 300, Name: "check_1", Description: "desc_2", Every: mustDuration(t, 2*time.Minute), Offset: mustDuration(t, 30*time.Second), Query: influxdb.DashboardQuery{ Text: `from(bucket: "telegraf") |> range(start: -1m) |> filter(fn: (r) => r._field == "usage_user")`, }, StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }", Tags: []influxdb.Tag{ {Key: "key_1", Value: "val_1"}, {Key: "key_2", Value: "val_2"}, }, }, TimeSince: mustDuration(t, time.Hour), StaleTime: mustDuration(t, 5*time.Hour), ReportZero: true, Level: notification.Critical, }, } tests := []struct { name string findName string findID platform.ID expected []influxdb.Check }{ { name: "find check with unique name", findName: "check_0", expected: []influxdb.Check{knownChecks[0]}, }, { name: "find multiple checks with same name", findName: "check_1", expected: []influxdb.Check{knownChecks[1], knownChecks[2]}, }, { name: "find no checks", findName: "fakeCheck", expected: nil, }, { name: "find check by id", findID: platform.ID(1), expected: []influxdb.Check{knownChecks[1]}, }, { name: "find check by id, set new name", findID: platform.ID(1), findName: "chex original", expected: []influxdb.Check{knownChecks[1]}, }, } for _, tt := range tests { fn := func(t *testing.T) { checkSVC := mock.NewCheckService() checkSVC.FindChecksFn = func(_ context.Context, filter influxdb.CheckFilter, _ ...influxdb.FindOptions) ([]influxdb.Check, int, error) { if filter.ID != nil { for i := range knownChecks { if knownChecks[i].GetID() == *filter.ID { return []influxdb.Check{knownChecks[i]}, 1, nil } } return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) } else if filter.Name != nil { checks := []influxdb.Check{} for i := range knownChecks { if knownChecks[i].GetName() == *filter.Name { checks = append(checks, knownChecks[i]) } } if lChecks := len(checks); lChecks > 0 { return checks, lChecks, nil } return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) } return knownChecks, len(knownChecks), nil } resToClone := ResourceToClone{ Kind: KindCheck, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService(WithCheckSVC(checkSVC)) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].SetName(tt.findName) } actual := template.Summary().Checks require.Len(t, actual, len(tt.expected)) sort.Slice(actual, func(i, j int) bool { return actual[i].Check.GetDescription() < actual[j].Check.GetDescription() }) for i := range actual { assert.Equal(t, tt.expected[i].GetName(), actual[i].Check.GetName()) assert.Equal(t, tt.expected[i].GetDescription(), actual[i].Check.GetDescription()) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) newQuery := func() influxdb.DashboardQuery { return influxdb.DashboardQuery{ Text: "from(v.bucket) |> count()", EditMode: "advanced", } } newAxes := func() map[string]influxdb.Axis { return map[string]influxdb.Axis{ "x": { Bounds: []string{}, Label: "labx", Prefix: "pre", Suffix: "suf", Base: "base", Scale: "linear", }, "y": { Bounds: []string{}, Label: "laby", Prefix: "pre", Suffix: "suf", Base: "base", Scale: "linear", }, } } newColors := func(types ...string) []influxdb.ViewColor { var out []influxdb.ViewColor for _, t := range types { out = append(out, influxdb.ViewColor{ Type: t, Hex: time.Now().Format(time.RFC3339), Name: time.Now().Format(time.RFC3339), Value: float64(time.Now().Unix()), }) } return out } t.Run("dashboard", func(t *testing.T) { t.Run("with single chart", func(t *testing.T) { tests := []struct { name string newName string expectedView influxdb.View }{ { name: "gauge", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.GaugeViewProperties{ Type: influxdb.ViewPropertyTypeGauge, DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, Note: "a note", Prefix: "pre", TickPrefix: "true", Suffix: "suf", TickSuffix: "false", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: newColors("min", "max", "threshold"), }, }, }, { name: "geo", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.GeoViewProperties{ Type: influxdb.ViewPropertyTypeGeo, Queries: []influxdb.DashboardQuery{newQuery()}, Center: influxdb.Datum{Lat: 10, Lon: -4}, Zoom: 4, GeoLayers: []influxdb.GeoLayer{{ Type: "circleMap", RadiusField: "radius", IntensityField: "count", Radius: 5, Blur: 1, RadiusDimension: influxdb.Axis{ Prefix: "$", Suffix: "%", }, InterpolateColors: false, TrackWidth: 4, Speed: 1, RandomColors: false, IsClustered: false, ViewColors: newColors("min", "max"), }}, Note: "a note", ShowNoteWhenEmpty: true, }, }, }, { name: "heatmap", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.HeatmapViewProperties{ Type: influxdb.ViewPropertyTypeHeatMap, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: []string{"#8F8AF4", "#8F8AF4", "#8F8AF4"}, XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YColumn: "y", GenerateYAxisTicks: []string{"yTotalTicks", "yTickStart", "yTickStep"}, YTotalTicks: 10, YTickStart: 0, YTickStep: 100, XDomain: []float64{0, 10}, YDomain: []float64{0, 100}, XAxisLabel: "x_label", XPrefix: "x_prefix", XSuffix: "x_suffix", YAxisLabel: "y_label", YPrefix: "y_prefix", YSuffix: "y_suffix", BinSize: 10, TimeFormat: "", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "histogram", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.HistogramViewProperties{ Type: influxdb.ViewPropertyTypeHistogram, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}}, FillColumns: []string{"a", "b"}, XColumn: "_value", XDomain: []float64{0, 10}, XAxisLabel: "x_label", BinCount: 30, Position: "stacked", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "scatter", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.ScatterViewProperties{ Type: influxdb.ViewPropertyTypeScatter, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: []string{"#8F8AF4", "#8F8AF4", "#8F8AF4"}, XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YColumn: "y", GenerateYAxisTicks: []string{"yTotalTicks", "yTickStart", "yTickStep"}, YTotalTicks: 10, YTickStart: 0, YTickStep: 100, XDomain: []float64{0, 10}, YDomain: []float64{0, 100}, XAxisLabel: "x_label", XPrefix: "x_prefix", XSuffix: "x_suffix", YAxisLabel: "y_label", YPrefix: "y_prefix", YSuffix: "y_suffix", TimeFormat: "", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "mosaic", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.MosaicViewProperties{ Type: influxdb.ViewPropertyTypeMosaic, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: []string{"#8F8AF4", "#8F8AF4", "#8F8AF4"}, HoverDimension: "y", XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YLabelColumnSeparator: ",", YLabelColumns: []string{"foo"}, YSeriesColumns: []string{"y"}, XDomain: []float64{0, 10}, YDomain: []float64{0, 100}, XAxisLabel: "x_label", XPrefix: "x_prefix", XSuffix: "x_suffix", YAxisLabel: "y_label", YPrefix: "y_prefix", YSuffix: "y_suffix", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "without new name single stat", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.SingleStatViewProperties{ Type: influxdb.ViewPropertyTypeSingleStat, DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, Prefix: "pre", TickPrefix: "false", ShowNoteWhenEmpty: true, Suffix: "suf", TickSuffix: "true", ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, }, }, }, { name: "with new name single stat", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.SingleStatViewProperties{ Type: influxdb.ViewPropertyTypeSingleStat, DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, Prefix: "pre", TickPrefix: "false", ShowNoteWhenEmpty: true, Suffix: "suf", TickSuffix: "true", ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, }, }, }, { name: "single stat plus line", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.LinePlusSingleStatProperties{ Type: influxdb.ViewPropertyTypeSingleStatPlusLine, Axes: newAxes(), DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, StaticLegend: influxdb.StaticLegend{ColorizeRows: true, HeightRatio: 0.2, Show: true, Opacity: 1.0, OrientationThreshold: 5, ValueAxis: "y", WidthRatio: 1.0}, Note: "a note", Prefix: "pre", Suffix: "suf", Queries: []influxdb.DashboardQuery{newQuery()}, ShadeBelow: true, HoverDimension: "y", ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YColumn: "y", GenerateYAxisTicks: []string{"yTotalTicks", "yTickStart", "yTickStep"}, YTotalTicks: 10, YTickStart: 0, YTickStep: 100, Position: "stacked", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "xy", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.XYViewProperties{ Type: influxdb.ViewPropertyTypeXY, Axes: newAxes(), Geom: "step", StaticLegend: influxdb.StaticLegend{ColorizeRows: true, HeightRatio: 0.2, Show: true, Opacity: 1.0, OrientationThreshold: 5, ValueAxis: "y", WidthRatio: 1.0}, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShadeBelow: true, HoverDimension: "y", ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YColumn: "y", GenerateYAxisTicks: []string{"yTotalTicks", "yTickStart", "yTickStep"}, YTotalTicks: 10, YTickStart: 0, YTickStep: 100, Position: "overlaid", TimeFormat: "", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "band", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.BandViewProperties{ Type: influxdb.ViewPropertyTypeBand, Axes: newAxes(), Geom: "step", StaticLegend: influxdb.StaticLegend{ColorizeRows: true, HeightRatio: 0.2, Show: true, Opacity: 1.0, OrientationThreshold: 5, ValueAxis: "y", WidthRatio: 1.0}, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, HoverDimension: "y", ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YColumn: "y", GenerateYAxisTicks: []string{"yTotalTicks", "yTickStart", "yTickStep"}, YTotalTicks: 10, YTickStart: 0, YTickStep: 100, UpperColumn: "upper", MainColumn: "main", LowerColumn: "lower", TimeFormat: "", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }, }, { name: "markdown", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.MarkdownViewProperties{ Type: influxdb.ViewPropertyTypeMarkdown, Note: "a note", }, }, }, { name: "table", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.TableViewProperties{ Type: influxdb.ViewPropertyTypeTable, Note: "a note", ShowNoteWhenEmpty: true, Queries: []influxdb.DashboardQuery{newQuery()}, ViewColors: []influxdb.ViewColor{{Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}}, TableOptions: influxdb.TableOptions{ VerticalTimeAxis: true, SortBy: influxdb.RenamableField{ InternalName: "_time", }, Wrapping: "truncate", FixFirstColumn: true, }, FieldOptions: []influxdb.RenamableField{ { InternalName: "_time", DisplayName: "time (ms)", Visible: true, }, }, TimeFormat: "YYYY:MM:DD", DecimalPlaces: influxdb.DecimalPlaces{ IsEnforced: true, Digits: 1, }, }, }, }, { // validate implementation resolves: https://github.com/influxdata/influxdb/issues/17708 name: "table converts table options correctly", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.TableViewProperties{ Type: influxdb.ViewPropertyTypeTable, Note: "a note", ShowNoteWhenEmpty: true, Queries: []influxdb.DashboardQuery{newQuery()}, ViewColors: []influxdb.ViewColor{{Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}}, TableOptions: influxdb.TableOptions{ VerticalTimeAxis: true, SortBy: influxdb.RenamableField{ InternalName: "_time", }, Wrapping: "truncate", }, FieldOptions: []influxdb.RenamableField{ { InternalName: "_time", DisplayName: "time (ms)", Visible: true, }, { InternalName: "_value", DisplayName: "bytes", Visible: true, }, }, TimeFormat: "YYYY:MM:DD", DecimalPlaces: influxdb.DecimalPlaces{ IsEnforced: true, Digits: 1, }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { expectedCell := &influxdb.Cell{ ID: 5, CellProperty: influxdb.CellProperty{X: 1, Y: 2, W: 3, H: 4}, View: &tt.expectedView, } expected := &influxdb.Dashboard{ ID: 3, Name: "bucket name", Description: "desc", Cells: []*influxdb.Cell{expectedCell}, } dashSVC := mock.NewDashboardService() dashSVC.FindDashboardByIDF = func(_ context.Context, id platform.ID) (*influxdb.Dashboard, error) { if id != expected.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expected, nil } dashSVC.FindDashboardsF = func(_ context.Context, filter influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { if len(filter.IDs) < 1 { return nil, 0, errors.New("uh ohhh, no id here") } if filter.IDs[0] != nil && *filter.IDs[0] != expected.ID { return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.IDs[0].String()) } return []*influxdb.Dashboard{expected}, 1, nil } dashSVC.GetDashboardCellViewF = func(_ context.Context, id platform.ID, cID platform.ID) (*influxdb.View, error) { if id == expected.ID && cID == expectedCell.ID { return &tt.expectedView, nil } return nil, errors.New("wrongo ids") } svc := newTestService(WithDashboardSVC(dashSVC), WithLabelSVC(mock.NewLabelService())) resToClone := ResourceToClone{ Kind: KindDashboard, ID: expected.ID, Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) dashs := newTemplate.Summary().Dashboards require.Len(t, dashs, 1) actual := dashs[0] expectedName := expected.Name if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actual.Name) assert.Equal(t, expected.Description, actual.Description) require.Len(t, actual.Charts, 1) ch := actual.Charts[0] assert.Equal(t, int(expectedCell.X), ch.XPosition) assert.Equal(t, int(expectedCell.Y), ch.YPosition) assert.Equal(t, int(expectedCell.H), ch.Height) assert.Equal(t, int(expectedCell.W), ch.Width) assert.Equal(t, tt.expectedView.Properties, ch.Properties) } t.Run(tt.name, fn) } }) t.Run("handles duplicate dashboard names", func(t *testing.T) { dashSVC := mock.NewDashboardService() dashSVC.FindDashboardByIDF = func(_ context.Context, id platform.ID) (*influxdb.Dashboard, error) { return &influxdb.Dashboard{ ID: id, Name: "dash name", Description: "desc", }, nil } dashSVC.FindDashboardsF = func(_ context.Context, filter influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { if len(filter.IDs) < 1 { return nil, 0, errors.New("uh ohhh, no id here") } return []*influxdb.Dashboard{{ ID: *filter.IDs[0], Name: "dash name", Description: "desc", }}, 1, nil } svc := newTestService(WithDashboardSVC(dashSVC), WithLabelSVC(mock.NewLabelService())) resourcesToClone := []ResourceToClone{ { Kind: KindDashboard, ID: 1, }, { Kind: KindDashboard, ID: 2, }, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) dashs := newTemplate.Summary().Dashboards require.Len(t, dashs, len(resourcesToClone)) for i := range resourcesToClone { actual := dashs[i] assert.Equal(t, "dash name", actual.Name) assert.Equal(t, "desc", actual.Description) } }) }) t.Run("dashboard by name", func(t *testing.T) { id := 0 newDash := func(name string, view influxdb.View) *influxdb.Dashboard { id++ return &influxdb.Dashboard{ ID: platform.ID(id), Name: name, Description: fmt.Sprintf("desc_%d", id), Cells: []*influxdb.Cell{ { ID: 0, CellProperty: influxdb.CellProperty{X: 1, Y: 2, W: 3, H: 4}, View: &view, }, }, } } knownDashboards := []*influxdb.Dashboard{ newDash("dasher", influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.GaugeViewProperties{ Type: influxdb.ViewPropertyTypeGauge, DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, Note: "a note", Prefix: "pre", TickPrefix: "true", Suffix: "suf", TickSuffix: "false", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: newColors("min", "max", "threshold"), }, }), newDash("prancer", influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.HeatmapViewProperties{ Type: influxdb.ViewPropertyTypeHeatMap, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: []string{"#8F8AF4", "#8F8AF4", "#8F8AF4"}, XColumn: "x", GenerateXAxisTicks: []string{"xTotalTicks", "xTickStart", "xTickStep"}, XTotalTicks: 15, XTickStart: 0, XTickStep: 1000, YColumn: "y", GenerateYAxisTicks: []string{"yTotalTicks", "yTickStart", "yTickStep"}, YTotalTicks: 10, YTickStart: 0, YTickStep: 100, XDomain: []float64{0, 10}, YDomain: []float64{0, 100}, XAxisLabel: "x_label", XPrefix: "x_prefix", XSuffix: "x_suffix", YAxisLabel: "y_label", YPrefix: "y_prefix", YSuffix: "y_suffix", BinSize: 10, TimeFormat: "", }, }), newDash("prancer", influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.HistogramViewProperties{ Type: influxdb.ViewPropertyTypeHistogram, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}}, FillColumns: []string{"a", "b"}, XColumn: "_value", XDomain: []float64{0, 10}, XAxisLabel: "x_label", BinCount: 30, Position: "stacked", LegendColorizeRows: true, LegendHide: false, LegendOpacity: 1.0, LegendOrientationThreshold: 5, }, }), } tests := []struct { name string findName string findID platform.ID expected []*influxdb.Dashboard }{ { name: "find dash with unique name", findName: "dasher", expected: []*influxdb.Dashboard{knownDashboards[0]}, }, { name: "find multiple dash with shared name", findName: "prancer", expected: []*influxdb.Dashboard{knownDashboards[1], knownDashboards[2]}, }, { name: "find no dash", findName: "fakeDash", expected: nil, }, { name: "find dash by id", findID: 1, expected: []*influxdb.Dashboard{knownDashboards[0]}, }, { name: "find dash by id, set new name", findID: 1, findName: "dancer", expected: []*influxdb.Dashboard{knownDashboards[0]}, }, } for _, tt := range tests { fn := func(t *testing.T) { dashSVC := mock.NewDashboardService() dashSVC.FindDashboardsF = func(_ context.Context, filter influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { if filter.IDs != nil && filter.IDs[0] != nil { for i := range knownDashboards { if knownDashboards[i].ID == *filter.IDs[0] { return []*influxdb.Dashboard{knownDashboards[i]}, 1, nil } } return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.IDs[0].String()) } return knownDashboards, len(knownDashboards), nil } dashSVC.GetDashboardCellViewF = func(_ context.Context, id platform.ID, cID platform.ID) (*influxdb.View, error) { for i := range knownDashboards { if knownDashboards[i].ID == id { return knownDashboards[i].Cells[0].View, nil } } return nil, errors.New("wrongo ids") } resToClone := ResourceToClone{ Kind: KindDashboard, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService( WithDashboardSVC(dashSVC), WithLabelSVC(mock.NewLabelService()), ) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].Name = tt.findName } actual := template.Summary().Dashboards require.Len(t, actual, len(tt.expected)) sort.Slice(actual, func(i, j int) bool { return actual[i].Description < actual[j].Description }) for i := range actual { assert.Equal(t, tt.expected[i].Name, actual[i].Name) assert.Equal(t, tt.expected[i].Description, actual[i].Description) require.Len(t, actual[i].Charts, 1) ch := actual[i].Charts[0] assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.X), ch.XPosition) assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.Y), ch.YPosition) assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.H), ch.Height) assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.W), ch.Width) assert.Equal(t, tt.expected[i].Cells[0].View.Properties, ch.Properties) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("label", func(t *testing.T) { tests := []struct { name string newName string }{ { name: "without new name", }, { name: "with new name", newName: "new name", }, } for _, tt := range tests { fn := func(t *testing.T) { expectedLabel := &influxdb.Label{ ID: 3, Name: "bucket name", Properties: map[string]string{ "description": "desc", "color": "red", }, } labelSVC := mock.NewLabelService() labelSVC.FindLabelByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Label, error) { if id != expectedLabel.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expectedLabel, nil } labelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { if filter.Name != expectedLabel.Name { return nil, errors.New("uh ohhh, wrong name here: " + filter.Name) } return []*influxdb.Label{expectedLabel}, nil } svc := newTestService(WithLabelSVC(labelSVC)) resToClone := ResourceToClone{ Kind: KindLabel, ID: expectedLabel.ID, Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) newLabels := newTemplate.Summary().Labels require.Len(t, newLabels, 1) actual := newLabels[0] expectedName := expectedLabel.Name if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actual.Name) assert.Equal(t, expectedLabel.Properties["color"], actual.Properties.Color) assert.Equal(t, expectedLabel.Properties["description"], actual.Properties.Description) } t.Run(tt.name, fn) } }) t.Run("label by name", func(t *testing.T) { knownLabels := []*influxdb.Label{ { ID: 1, Name: "label one", Properties: map[string]string{ "description": "desc", "color": "red", }, }, { ID: 2, Name: "label two", Properties: map[string]string{ "description": "desc2", "color": "green", }, }, } tests := []struct { name string findName string findID platform.ID expected []*influxdb.Label }{ { name: "find label by name", findName: "label one", expected: []*influxdb.Label{knownLabels[0]}, }, { name: "find no label", findName: "label none", expected: nil, }, { name: "find label by id", findID: platform.ID(2), expected: []*influxdb.Label{knownLabels[1]}, }, { name: "find label by id, set new name", findName: "label three", findID: platform.ID(2), expected: []*influxdb.Label{knownLabels[1]}, }, } for _, tt := range tests { fn := func(t *testing.T) { labelSVC := mock.NewLabelService() labelSVC.FindLabelByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Label, error) { for i := range knownLabels { if knownLabels[i].ID == id { return knownLabels[i], nil } } return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } labelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { if filter.Name != "" { for i := range knownLabels { if knownLabels[i].Name == filter.Name { return []*influxdb.Label{knownLabels[i]}, nil } } return nil, errors.New("uh ohhh, wrong name here: " + filter.Name) } return knownLabels, nil } resToClone := ResourceToClone{ Kind: KindLabel, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService(WithLabelSVC(labelSVC)) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].Name = tt.findName } actual := template.Summary().Labels require.Len(t, actual, len(tt.expected)) for i := range actual { assert.Equal(t, tt.expected[i].Name, actual[i].Name) assert.Equal(t, tt.expected[i].Properties["color"], actual[i].Properties.Color) assert.Equal(t, tt.expected[i].Properties["description"], actual[i].Properties.Description) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("notification endpoints", func(t *testing.T) { tests := []struct { name string newName string expected influxdb.NotificationEndpoint }{ { name: "pager duty", expected: &endpoint.PagerDuty{ Base: endpoint.Base{ Name: "pd-endpoint", Description: "desc", Status: taskmodel.TaskStatusActive, }, ClientURL: "http://example.com", RoutingKey: influxdb.SecretField{Key: "-routing-key"}, }, }, { name: "pager duty with new name", newName: "new name", expected: &endpoint.PagerDuty{ Base: endpoint.Base{ Name: "pd-endpoint", Description: "desc", Status: taskmodel.TaskStatusActive, }, ClientURL: "http://example.com", RoutingKey: influxdb.SecretField{Key: "-routing-key"}, }, }, { name: "slack", expected: &endpoint.Slack{ Base: endpoint.Base{ Name: "pd-endpoint", Description: "desc", Status: taskmodel.TaskStatusInactive, }, URL: "http://example.com", Token: influxdb.SecretField{Key: "tokne"}, }, }, { name: "http basic", expected: &endpoint.HTTP{ Base: endpoint.Base{ Name: "pd-endpoint", Description: "desc", Status: taskmodel.TaskStatusInactive, }, AuthMethod: "basic", Method: "POST", URL: "http://example.com", Password: influxdb.SecretField{Key: "password"}, Username: influxdb.SecretField{Key: "username"}, }, }, { name: "http bearer", expected: &endpoint.HTTP{ Base: endpoint.Base{ Name: "pd-endpoint", Description: "desc", Status: taskmodel.TaskStatusInactive, }, AuthMethod: "bearer", Method: "GET", URL: "http://example.com", Token: influxdb.SecretField{Key: "token"}, }, }, { name: "http none", expected: &endpoint.HTTP{ Base: endpoint.Base{ Name: "pd-endpoint", Description: "desc", Status: taskmodel.TaskStatusInactive, }, AuthMethod: "none", Method: "GET", URL: "http://example.com", }, }, } for _, tt := range tests { fn := func(t *testing.T) { id := platform.ID(1) tt.expected.SetID(id) endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationEndpoint, error) { if id != tt.expected.GetID() { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return tt.expected, nil } endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, filter influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { if filter.ID != nil && *filter.ID != tt.expected.GetID() { return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) } return []influxdb.NotificationEndpoint{tt.expected}, 1, nil } svc := newTestService(WithNotificationEndpointSVC(endpointSVC)) resToClone := ResourceToClone{ Kind: KindNotificationEndpoint, ID: tt.expected.GetID(), Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) endpoints := newTemplate.Summary().NotificationEndpoints require.Len(t, endpoints, 1) actual := endpoints[0].NotificationEndpoint expectedName := tt.expected.GetName() if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actual.GetName()) assert.Equal(t, tt.expected.GetDescription(), actual.GetDescription()) assert.Equal(t, tt.expected.GetStatus(), actual.GetStatus()) assert.Equal(t, tt.expected.SecretFields(), actual.SecretFields()) } t.Run(tt.name, fn) } }) knownEndpoints := []influxdb.NotificationEndpoint{ &endpoint.PagerDuty{ Base: endpoint.Base{ ID: newTestIDPtr(1), Name: "pd endpoint", Description: "desc", Status: taskmodel.TaskStatusActive, }, ClientURL: "http://example.com", RoutingKey: influxdb.SecretField{Key: "-routing-key"}, }, &endpoint.PagerDuty{ Base: endpoint.Base{ ID: newTestIDPtr(2), Name: "pd-endpoint", Description: "desc pd", Status: taskmodel.TaskStatusActive, }, ClientURL: "http://example.com", RoutingKey: influxdb.SecretField{Key: "-routing-key"}, }, &endpoint.Slack{ Base: endpoint.Base{ ID: newTestIDPtr(3), Name: "slack endpoint", Description: "desc slack", Status: taskmodel.TaskStatusInactive, }, URL: "http://example.com", Token: influxdb.SecretField{Key: "tokne"}, }, } t.Run("notification endpoints by name", func(t *testing.T) { tests := []struct { name string findName string findID platform.ID expected []influxdb.NotificationEndpoint }{ { name: "find notification endpoint with unique name", findName: "pd endpoint", expected: []influxdb.NotificationEndpoint{knownEndpoints[0]}, }, { name: "find no notification endpoints", findName: "fakeEndpoint", expected: nil, }, { name: "find notification endpoint by id", findID: platform.ID(2), expected: []influxdb.NotificationEndpoint{knownEndpoints[1]}, }, { name: "find by id, set new name", findID: platform.ID(3), findName: "slack-endpoint", expected: []influxdb.NotificationEndpoint{knownEndpoints[2]}, }, } for _, tt := range tests { fn := func(t *testing.T) { endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationEndpoint, error) { for i := range knownEndpoints { if knownEndpoints[i].GetID() == id { return knownEndpoints[i], nil } } return nil, errors.New("uh ohhh, wrong endpoint id here: " + id.String()) } endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, filter influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { if filter.ID != nil { for i := range knownEndpoints { if knownEndpoints[i].GetID() == *filter.ID { return []influxdb.NotificationEndpoint{knownEndpoints[i]}, 1, nil } } return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) } return knownEndpoints, len(knownEndpoints), nil } resToClone := ResourceToClone{ Kind: KindNotificationEndpoint, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService(WithNotificationEndpointSVC(endpointSVC)) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].SetName(tt.findName) } actual := template.Summary().NotificationEndpoints require.Len(t, actual, len(tt.expected)) for i := range actual { assert.Equal(t, tt.expected[i].GetName(), actual[i].NotificationEndpoint.GetName()) assert.Equal(t, tt.expected[i].GetDescription(), actual[i].NotificationEndpoint.GetDescription()) assert.Equal(t, tt.expected[i].GetStatus(), actual[i].NotificationEndpoint.GetStatus()) assert.Equal(t, tt.expected[i].SecretFields(), actual[i].NotificationEndpoint.SecretFields()) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("notification rules", func(t *testing.T) { newRuleBase := func(id int) rule.Base { return rule.Base{ ID: platform.ID(id), Name: "old_name", Description: "desc", EndpointID: platform.ID(id), Every: mustDuration(t, time.Hour), Offset: mustDuration(t, time.Minute), TagRules: []notification.TagRule{ {Tag: influxdb.Tag{Key: "k1", Value: "v1"}}, }, StatusRules: []notification.StatusRule{ {CurrentLevel: notification.Ok, PreviousLevel: levelPtr(notification.Warn)}, {CurrentLevel: notification.Critical}, }, } } t.Run("single rule export", func(t *testing.T) { tests := []struct { name string newName string endpoint influxdb.NotificationEndpoint rule influxdb.NotificationRule }{ { name: "pager duty", newName: "pager_duty_name", endpoint: &endpoint.PagerDuty{ Base: endpoint.Base{ ID: newTestIDPtr(13), Name: "endpoint_0", Description: "desc", Status: taskmodel.TaskStatusActive, }, ClientURL: "http://example.com", RoutingKey: influxdb.SecretField{Key: "-routing-key"}, }, rule: &rule.PagerDuty{ Base: newRuleBase(13), MessageTemplate: "Template", }, }, { name: "slack", endpoint: &endpoint.Slack{ Base: endpoint.Base{ ID: newTestIDPtr(13), Name: "endpoint_0", Description: "desc", Status: taskmodel.TaskStatusInactive, }, URL: "http://example.com", Token: influxdb.SecretField{Key: "tokne"}, }, rule: &rule.Slack{ Base: newRuleBase(13), Channel: "abc", MessageTemplate: "SLACK TEMPlate", }, }, { name: "http none", endpoint: &endpoint.HTTP{ Base: endpoint.Base{ ID: newTestIDPtr(13), Name: "endpoint_0", Description: "desc", Status: taskmodel.TaskStatusInactive, }, AuthMethod: "none", Method: "GET", URL: "http://example.com", }, rule: &rule.HTTP{ Base: newRuleBase(13), }, }, } for _, tt := range tests { fn := func(t *testing.T) { endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationEndpoint, error) { if id != tt.endpoint.GetID() { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return tt.endpoint, nil } ruleSVC := mock.NewNotificationRuleStore() ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationRule, error) { return tt.rule, nil } ruleSVC.FindNotificationRulesF = func(ctx context.Context, _ influxdb.NotificationRuleFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationRule, int, error) { return []influxdb.NotificationRule{tt.rule}, 1, nil } svc := newTestService( WithNotificationEndpointSVC(endpointSVC), WithNotificationRuleSVC(ruleSVC), ) resToClone := ResourceToClone{ Kind: KindNotificationRule, ID: tt.rule.GetID(), Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() require.Len(t, sum.NotificationRules, 1) actualRule := sum.NotificationRules[0] assert.Zero(t, actualRule.ID) assert.Zero(t, actualRule.EndpointID) assert.NotEmpty(t, actualRule.EndpointType) assert.NotEmpty(t, actualRule.EndpointMetaName) baseEqual := func(t *testing.T, base rule.Base) { t.Helper() expectedName := base.Name if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actualRule.Name) assert.Equal(t, base.Description, actualRule.Description) assert.Equal(t, base.Every.TimeDuration().String(), actualRule.Every) assert.Equal(t, base.Offset.TimeDuration().String(), actualRule.Offset) for _, sRule := range base.StatusRules { expected := SummaryStatusRule{CurrentLevel: sRule.CurrentLevel.String()} if sRule.PreviousLevel != nil { expected.PreviousLevel = sRule.PreviousLevel.String() } assert.Contains(t, actualRule.StatusRules, expected) } for _, tRule := range base.TagRules { expected := SummaryTagRule{ Key: tRule.Key, Value: tRule.Value, Operator: tRule.Operator.String(), } assert.Contains(t, actualRule.TagRules, expected) } } switch p := tt.rule.(type) { case *rule.HTTP: baseEqual(t, p.Base) case *rule.PagerDuty: baseEqual(t, p.Base) assert.Equal(t, p.MessageTemplate, actualRule.MessageTemplate) case *rule.Slack: baseEqual(t, p.Base) assert.Equal(t, p.MessageTemplate, actualRule.MessageTemplate) } require.Len(t, template.Summary().NotificationEndpoints, 1) actualEndpoint := template.Summary().NotificationEndpoints[0].NotificationEndpoint assert.Equal(t, tt.endpoint.GetName(), actualEndpoint.GetName()) assert.Equal(t, tt.endpoint.GetDescription(), actualEndpoint.GetDescription()) assert.Equal(t, tt.endpoint.GetStatus(), actualEndpoint.GetStatus()) } t.Run(tt.name, fn) } }) t.Run("handles rules duplicate names", func(t *testing.T) { endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationEndpoint, error) { return &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "endpoint_0", Description: "desc", Status: taskmodel.TaskStatusInactive, }, AuthMethod: "none", Method: "GET", URL: "http://example.com", }, nil } ruleSVC := mock.NewNotificationRuleStore() ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationRule, error) { return &rule.HTTP{ Base: newRuleBase(int(id)), }, nil } svc := newTestService( WithNotificationEndpointSVC(endpointSVC), WithNotificationRuleSVC(ruleSVC), ) resourcesToClone := []ResourceToClone{ { Kind: KindNotificationRule, ID: 1, }, { Kind: KindNotificationRule, ID: 2, }, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() require.Len(t, sum.NotificationRules, len(resourcesToClone)) expectedSameEndpointName := sum.NotificationRules[0].EndpointMetaName assert.NotZero(t, expectedSameEndpointName) assert.NotEqual(t, "endpoint_0", expectedSameEndpointName) for i := range resourcesToClone { actual := sum.NotificationRules[i] assert.Equal(t, "old_name", actual.Name) assert.Equal(t, "desc", actual.Description) assert.Equal(t, expectedSameEndpointName, actual.EndpointMetaName) } require.Len(t, sum.NotificationEndpoints, 1) assert.Equal(t, "endpoint_0", sum.NotificationEndpoints[0].NotificationEndpoint.GetName()) }) }) t.Run("notification rules by name", func(t *testing.T) { newRuleBase := func(id int, name string) rule.Base { return rule.Base{ ID: platform.ID(id), Name: name, Description: fmt.Sprintf("desc %d", id), EndpointID: platform.ID(1), // todo: setting to id as well likely doesn't work due to safeID Every: mustDuration(t, time.Hour), Offset: mustDuration(t, time.Minute), TagRules: []notification.TagRule{ {Tag: influxdb.Tag{Key: "k1", Value: "v1"}}, }, StatusRules: []notification.StatusRule{ {CurrentLevel: notification.Ok, PreviousLevel: levelPtr(notification.Warn)}, {CurrentLevel: notification.Critical}, }, } } knownRules := []influxdb.NotificationRule{ &rule.PagerDuty{ Base: newRuleBase(1, "pd notify"), MessageTemplate: "Template", }, &rule.PagerDuty{ Base: newRuleBase(2, "pd-notify"), MessageTemplate: "Template2 ", }, &rule.Slack{ Base: newRuleBase(3, "pd-notify"), Channel: "abc", MessageTemplate: "SLACK TEMPlate", }, } tests := []struct { name string findName string findID platform.ID expected []influxdb.NotificationRule }{ { name: "find rule with unique name", findName: "pd notify", expected: []influxdb.NotificationRule{knownRules[0]}, }, { name: "find multiple rules with shared name", findName: "pd-notify", expected: []influxdb.NotificationRule{knownRules[1], knownRules[2]}, }, { name: "find no rules", findName: "fakeRule", expected: nil, }, { name: "find rule by id", findID: platform.ID(2), expected: []influxdb.NotificationRule{knownRules[1]}, }, { name: "find by id, set new name", findID: platform.ID(3), findName: "slack-notify", expected: []influxdb.NotificationRule{knownRules[2]}, }, } for _, tt := range tests { fn := func(t *testing.T) { endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationEndpoint, error) { for i := range knownEndpoints { if knownEndpoints[i].GetID() == id { return knownEndpoints[i], nil } } return nil, errors.New("uh ohhh, wrong endpoint id here: " + id.String()) } ruleSVC := mock.NewNotificationRuleStore() ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationRule, error) { for i := range knownRules { if knownRules[i].GetID() == id { return knownRules[i], nil } } return nil, errors.New("uh ohhh, wrong rule id here: " + id.String()) } ruleSVC.FindNotificationRulesF = func(ctx context.Context, _ influxdb.NotificationRuleFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationRule, int, error) { return knownRules, len(knownRules), nil } resToClone := ResourceToClone{ Kind: KindNotificationRule, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService( WithNotificationEndpointSVC(endpointSVC), WithNotificationRuleSVC(ruleSVC), ) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) actual := template.Summary() require.Len(t, actual.NotificationRules, len(tt.expected)) require.Len(t, actual.NotificationEndpoints, 1) sort.Slice(actual.NotificationRules, func(i, j int) bool { return actual.NotificationRules[i].Description < actual.NotificationRules[j].Description }) sort.Slice(actual.NotificationEndpoints, func(i, j int) bool { return actual.NotificationEndpoints[i].NotificationEndpoint.GetDescription() < actual.NotificationEndpoints[j].NotificationEndpoint.GetDescription() }) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].SetName(tt.findName) } for i := range actual.NotificationRules { assert.Zero(t, actual.NotificationRules[i].ID) assert.Zero(t, actual.NotificationRules[i].EndpointID) assert.NotEmpty(t, actual.NotificationRules[i].EndpointType) assert.NotEmpty(t, actual.NotificationRules[i].EndpointMetaName) baseEqual := func(t *testing.T, base rule.Base) { assert.Equal(t, base.Name, actual.NotificationRules[i].Name) assert.Equal(t, base.Description, actual.NotificationRules[i].Description) assert.Equal(t, base.Every.TimeDuration().String(), actual.NotificationRules[i].Every) assert.Equal(t, base.Offset.TimeDuration().String(), actual.NotificationRules[i].Offset) for _, sRule := range base.StatusRules { expected := SummaryStatusRule{CurrentLevel: sRule.CurrentLevel.String()} if sRule.PreviousLevel != nil { expected.PreviousLevel = sRule.PreviousLevel.String() } assert.Contains(t, actual.NotificationRules[i].StatusRules, expected) } for _, tRule := range base.TagRules { expected := SummaryTagRule{ Key: tRule.Key, Value: tRule.Value, Operator: tRule.Operator.String(), } assert.Contains(t, actual.NotificationRules[i].TagRules, expected) } } switch p := tt.expected[i].(type) { case *rule.HTTP: baseEqual(t, p.Base) case *rule.PagerDuty: baseEqual(t, p.Base) assert.Equal(t, p.MessageTemplate, actual.NotificationRules[i].MessageTemplate) case *rule.Slack: baseEqual(t, p.Base) assert.Equal(t, p.MessageTemplate, actual.NotificationRules[i].MessageTemplate) } for j := range actual.NotificationEndpoints { endpoint, err := endpointSVC.FindNotificationEndpointByIDF(context.Background(), tt.expected[i].GetEndpointID()) require.NoError(t, err) assert.Equal(t, endpoint.GetName(), actual.NotificationEndpoints[j].NotificationEndpoint.GetName()) assert.Equal(t, endpoint.GetDescription(), actual.NotificationEndpoints[j].NotificationEndpoint.GetDescription()) assert.Equal(t, endpoint.GetStatus(), actual.NotificationEndpoints[j].NotificationEndpoint.GetStatus()) } } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("tasks", func(t *testing.T) { t.Run("single task exports", func(t *testing.T) { tests := []struct { name string newName string task taskmodel.Task }{ { name: "every offset is set", newName: "new name", task: taskmodel.Task{ ID: 1, Name: "name_9000", Every: time.Minute.String(), Offset: 10 * time.Second, Type: taskmodel.TaskSystemType, Flux: `option task = { name: "larry" } from(bucket: "rucket") |> yield()`, }, }, { name: "cron is set", task: taskmodel.Task{ ID: 1, Name: "name_0", Cron: "2 * * * *", Type: taskmodel.TaskSystemType, Flux: `option task = { name: "larry" } from(bucket: "rucket") |> yield()`, }, }, } for _, tt := range tests { fn := func(t *testing.T) { taskSVC := mock.NewTaskService() taskSVC.FindTaskByIDFn = func(ctx context.Context, id platform.ID) (*taskmodel.Task, error) { if id != tt.task.ID { return nil, errors.New("wrong id provided: " + id.String()) } return &tt.task, nil } taskSVC.FindTasksFn = func(ctx context.Context, filter taskmodel.TaskFilter) ([]*taskmodel.Task, int, error) { return []*taskmodel.Task{&tt.task}, 1, nil } svc := newTestService(WithTaskSVC(taskSVC)) resToClone := ResourceToClone{ Kind: KindTask, ID: tt.task.ID, Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() tasks := sum.Tasks require.Len(t, tasks, 1) expectedName := tt.task.Name if tt.newName != "" { expectedName = tt.newName } actual := tasks[0] assert.Equal(t, expectedName, actual.Name) assert.Equal(t, tt.task.Cron, actual.Cron) assert.Equal(t, tt.task.Description, actual.Description) assert.Equal(t, tt.task.Every, actual.Every) assert.Equal(t, durToStr(tt.task.Offset), actual.Offset) expectedQuery := `from(bucket: "rucket") |> yield()` assert.Equal(t, expectedQuery, actual.Query) } t.Run(tt.name, fn) } }) t.Run("handles multiple tasks of same name", func(t *testing.T) { taskSVC := mock.NewTaskService() taskSVC.FindTaskByIDFn = func(ctx context.Context, id platform.ID) (*taskmodel.Task, error) { return &taskmodel.Task{ ID: id, Type: taskmodel.TaskSystemType, Name: "same name", Description: "desc", Status: taskmodel.TaskStatusActive, Flux: `from(bucket: "foo")`, Every: "5m0s", }, nil } svc := newTestService(WithTaskSVC(taskSVC)) resourcesToClone := []ResourceToClone{ { Kind: KindTask, ID: 1, }, { Kind: KindTask, ID: 2, }, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() tasks := sum.Tasks require.Len(t, tasks, len(resourcesToClone)) for _, actual := range sum.Tasks { assert.Equal(t, "same name", actual.Name) assert.Equal(t, "desc", actual.Description) assert.Equal(t, influxdb.Active, actual.Status) assert.Equal(t, `from(bucket: "foo")`, actual.Query) assert.Equal(t, "5m0s", actual.Every) } }) }) t.Run("tasks by name", func(t *testing.T) { knownTasks := []*taskmodel.Task{ { ID: 1, Name: "task", Description: "task 1", Every: time.Minute.String(), Offset: 10 * time.Second, Type: taskmodel.TaskSystemType, Flux: `option task = { name: "larry" } from(bucket: "rucket") |> yield()`, }, { ID: 2, Name: "taskCopy", Description: "task 2", Cron: "2 * * * *", Type: taskmodel.TaskSystemType, Flux: `option task = { name: "curly" } from(bucket: "rucket") |> yield()`, }, { ID: 3, Name: "taskCopy", Description: "task 3", Cron: "2 3 4 5 *", Type: taskmodel.TaskSystemType, Flux: `option task = { name: "moe" } from(bucket: "rucket") |> yield()`, }, } tests := []struct { name string findName string findID platform.ID expected []*taskmodel.Task }{ { name: "find task with unique name", findName: "task", expected: []*taskmodel.Task{knownTasks[0]}, }, { name: "find multiple tasks with shared name", findName: "taskCopy", expected: []*taskmodel.Task{knownTasks[1], knownTasks[2]}, }, { name: "find no tasks", findName: "faketask", expected: nil, }, { name: "find task by id", findID: platform.ID(2), expected: []*taskmodel.Task{knownTasks[1]}, }, { name: "find by id, set new name", findID: platform.ID(2), findName: "renamedTask", expected: []*taskmodel.Task{knownTasks[1]}, }, } for _, tt := range tests { fn := func(t *testing.T) { taskSVC := mock.NewTaskService() taskSVC.FindTaskByIDFn = func(ctx context.Context, id platform.ID) (*taskmodel.Task, error) { for i := range knownTasks { if knownTasks[i].ID == id { return knownTasks[i], nil } } return nil, errors.New("wrong id provided: " + id.String()) } taskSVC.FindTasksFn = func(ctx context.Context, filter taskmodel.TaskFilter) ([]*taskmodel.Task, int, error) { tasks := []*taskmodel.Task{} for i := range knownTasks { if knownTasks[i].Name == *filter.Name { tasks = append(tasks, knownTasks[i]) } } return tasks, len(tasks), nil } resToClone := ResourceToClone{ Kind: KindTask, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService(WithTaskSVC(taskSVC)) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].Name = tt.findName } actual := template.Summary().Tasks require.Len(t, actual, len(tt.expected)) sort.Slice(actual, func(i, j int) bool { return actual[i].Description < actual[j].Description }) for i := range actual { assert.Equal(t, tt.expected[i].Name, actual[i].Name) assert.Equal(t, tt.expected[i].Cron, actual[i].Cron) assert.Equal(t, tt.expected[i].Description, actual[i].Description) assert.Equal(t, tt.expected[i].Every, actual[i].Every) assert.Equal(t, durToStr(tt.expected[i].Offset), actual[i].Offset) assert.Equal(t, `from(bucket: "rucket") |> yield()`, actual[i].Query) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("telegraf configs", func(t *testing.T) { t.Run("allows for duplicate telegraf names to be exported", func(t *testing.T) { tConfig := &influxdb.TelegrafConfig{ OrgID: 9000, Name: "same name", Description: "desc", Config: "some config string", } teleStore := mock.NewTelegrafConfigStore() teleStore.FindTelegrafConfigByIDF = func(ctx context.Context, id platform.ID) (*influxdb.TelegrafConfig, error) { tConfig.ID = id return tConfig, nil } svc := newTestService(WithTelegrafSVC(teleStore)) resourcesToClone := []ResourceToClone{ { Kind: KindTelegraf, ID: 1, }, { Kind: KindTelegraf, ID: 2, }, } teleStore.FindTelegrafConfigsF = func(ctx context.Context, filter influxdb.TelegrafConfigFilter, _ ...influxdb.FindOptions) ([]*influxdb.TelegrafConfig, int, error) { tgrafs := []*influxdb.TelegrafConfig{} for _, r := range resourcesToClone { t := tConfig t.ID = r.ID tgrafs = append(tgrafs, t) } return tgrafs, len(tgrafs), nil } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() teles := sum.TelegrafConfigs sort.Slice(teles, func(i, j int) bool { return teles[i].TelegrafConfig.Name < teles[j].TelegrafConfig.Name }) require.Len(t, teles, len(resourcesToClone)) for i := range resourcesToClone { actual := teles[i] assert.Equal(t, "same name", actual.TelegrafConfig.Name) assert.Equal(t, "desc", actual.TelegrafConfig.Description) assert.Equal(t, "some config string", actual.TelegrafConfig.Config) } }) }) t.Run("telegraf configs by name", func(t *testing.T) { knownConfigs := []*influxdb.TelegrafConfig{ { ID: 1, OrgID: 9000, Name: "my config", Description: "desc1", Config: "a config string", }, { ID: 2, OrgID: 9000, Name: "telConfig", Description: "desc2", Config: "some config string", }, { ID: 3, OrgID: 9000, Name: "telConfig", Description: "desc3", Config: "some other config string", }, } tests := []struct { name string findName string findID platform.ID expected []*influxdb.TelegrafConfig }{ { name: "find telegraf with unique name", findName: "my config", expected: []*influxdb.TelegrafConfig{knownConfigs[0]}, }, { name: "find multiple telegrafs with shared name", findName: "telConfig", expected: []*influxdb.TelegrafConfig{knownConfigs[1], knownConfigs[2]}, }, { name: "find no telegrafs", findName: "fakeConfig", expected: nil, }, { name: "find telegraf by id", findID: platform.ID(2), expected: []*influxdb.TelegrafConfig{knownConfigs[1]}, }, { name: "find by id, set new name", findID: platform.ID(2), findName: "newConfig", expected: []*influxdb.TelegrafConfig{knownConfigs[1]}, }, } for _, tt := range tests { fn := func(t *testing.T) { teleStore := mock.NewTelegrafConfigStore() teleStore.FindTelegrafConfigByIDF = func(_ context.Context, id platform.ID) (*influxdb.TelegrafConfig, error) { for i := range knownConfigs { if knownConfigs[i].ID == id { return knownConfigs[i], nil } } return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } teleStore.FindTelegrafConfigsF = func(_ context.Context, filter influxdb.TelegrafConfigFilter, _ ...influxdb.FindOptions) ([]*influxdb.TelegrafConfig, int, error) { return knownConfigs, len(knownConfigs), nil } resToClone := ResourceToClone{ Kind: KindTelegraf, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService(WithTelegrafSVC(teleStore)) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].Name = tt.findName } actual := template.Summary().TelegrafConfigs require.Len(t, actual, len(tt.expected)) sort.Slice(actual, func(i, j int) bool { return actual[i].TelegrafConfig.Description < actual[j].TelegrafConfig.Description }) for i := range actual { assert.Equal(t, tt.expected[i].Name, actual[i].TelegrafConfig.Name) assert.Equal(t, tt.expected[i].Description, actual[i].TelegrafConfig.Description) assert.Equal(t, tt.expected[i].Config, actual[i].TelegrafConfig.Config) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("variable", func(t *testing.T) { tests := []struct { name string newName string expectedVar influxdb.Variable }{ { name: "without new name", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Description: "desc", Selected: []string{"val"}, Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"val"}, }, }, }, { name: "with new name", newName: "new name", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Selected: []string{"val"}, Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"val"}, }, }, }, { name: "with map arg", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Selected: []string{"v"}, Arguments: &influxdb.VariableArguments{ Type: "map", Values: influxdb.VariableMapValues{"k": "v"}, }, }, }, { name: "with query arg", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Selected: []string{"bucket-foo"}, Arguments: &influxdb.VariableArguments{ Type: "query", Values: influxdb.VariableQueryValues{ Query: "buckets()", Language: "flux", }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { varSVC := mock.NewVariableService() varSVC.FindVariableByIDF = func(_ context.Context, id platform.ID) (*influxdb.Variable, error) { if id != tt.expectedVar.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return &tt.expectedVar, nil } varSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { if filter.ID != nil && *filter.ID != tt.expectedVar.ID { return nil, errors.New("uh ohhh, wrong id here: " + fmt.Sprint(*filter.ID)) } return []*influxdb.Variable{&tt.expectedVar}, nil } svc := newTestService(WithVariableSVC(varSVC), WithLabelSVC(mock.NewLabelService())) resToClone := ResourceToClone{ Kind: KindVariable, ID: tt.expectedVar.ID, Name: tt.newName, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) newVars := newTemplate.Summary().Variables require.Len(t, newVars, 1) actual := newVars[0] expectedName := tt.expectedVar.Name if tt.newName != "" { expectedName = tt.newName } assert.Equal(t, expectedName, actual.Name) assert.Equal(t, tt.expectedVar.Description, actual.Description) assert.Equal(t, tt.expectedVar.Selected, actual.Selected) assert.Equal(t, tt.expectedVar.Arguments, actual.Arguments) } t.Run(tt.name, fn) } }) t.Run("variable by name", func(t *testing.T) { knownVariables := []*influxdb.Variable{ { ID: 1, Name: "variable", Description: "desc", Selected: []string{"val1"}, Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"val"}, }, }, { ID: 2, Name: "var 2", Selected: []string{"val2"}, Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"val"}, }, }, { ID: 3, Name: "var 3", Selected: []string{"v"}, Arguments: &influxdb.VariableArguments{ Type: "map", Values: influxdb.VariableMapValues{"k": "v"}, }, }, } tests := []struct { name string findName string findID platform.ID expected []*influxdb.Variable }{ { name: "find variable with unique name", findName: "variable", expected: []*influxdb.Variable{knownVariables[0]}, }, { name: "find no variables", findName: "fakeVariable", expected: nil, }, { name: "find variable by id", findID: platform.ID(2), expected: []*influxdb.Variable{knownVariables[1]}, }, { name: "find by id, set new name", findID: platform.ID(2), findName: "useful var", expected: []*influxdb.Variable{knownVariables[1]}, }, } for _, tt := range tests { fn := func(t *testing.T) { varSVC := mock.NewVariableService() varSVC.FindVariableByIDF = func(_ context.Context, id platform.ID) (*influxdb.Variable, error) { for i := range knownVariables { if knownVariables[i].ID == id { return knownVariables[i], nil } } return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } varSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { return knownVariables, nil } resToClone := ResourceToClone{ Kind: KindVariable, } if tt.findName != "" { resToClone.Name = tt.findName } if tt.findID != platform.ID(0) { resToClone.ID = tt.findID } svc := newTestService(WithVariableSVC(varSVC), WithLabelSVC(mock.NewLabelService())) template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) if tt.expected == nil { require.Error(t, err) } else { require.NoError(t, err) if tt.findName != "" && tt.findID != platform.ID(0) { tt.expected[0].Name = tt.findName } actual := template.Summary().Variables require.Len(t, actual, len(tt.expected)) for i := range actual { assert.Equal(t, tt.expected[i].Name, actual[i].Name) assert.Equal(t, tt.expected[i].Description, actual[i].Description) assert.Equal(t, tt.expected[i].Arguments, actual[i].Arguments) } assert.True(t, encodeAndDecode(t, template) != nil) } } t.Run(tt.name, fn) } }) t.Run("includes resource associations", func(t *testing.T) { t.Run("single resource with single association", func(t *testing.T) { expected := &influxdb.Bucket{ ID: 3, Name: "bucket name", Description: "desc", RetentionPeriod: time.Hour, } bktSVC := mock.NewBucketService() bktSVC.FindBucketByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Bucket, error) { if id != expected.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expected, nil } bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { if f.ID != nil && *f.ID != expected.ID { return nil, 0, errors.New("not suppose to get here") } return []*influxdb.Bucket{expected}, 1, nil } labelSVC := mock.NewLabelService() labelSVC.FindResourceLabelsFn = func(_ context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { if f.ResourceID != expected.ID { return nil, errors.New("uh ohs wrong id: " + f.ResourceID.String()) } return []*influxdb.Label{ {Name: "label_1"}, }, nil } svc := newTestService(WithBucketSVC(bktSVC), WithLabelSVC(labelSVC)) resToClone := ResourceToClone{ Kind: KindBucket, ID: expected.ID, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() bkts := sum.Buckets require.Len(t, bkts, 1) actual := bkts[0] expectedName := expected.Name assert.Equal(t, expectedName, actual.Name) assert.Equal(t, expected.Description, actual.Description) assert.Equal(t, expected.RetentionPeriod, actual.RetentionPeriod) require.Len(t, actual.LabelAssociations, 1) assert.Equal(t, "label_1", actual.LabelAssociations[0].Name) labels := sum.Labels require.Len(t, labels, 1) assert.Equal(t, "label_1", labels[0].Name) }) t.Run("multiple resources with same associations", func(t *testing.T) { bktSVC := mock.NewBucketService() bktSVC.FindBucketByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Bucket, error) { return &influxdb.Bucket{ID: id, Name: strconv.Itoa(int(id))}, nil } labelSVC := mock.NewLabelService() labelSVC.FindResourceLabelsFn = func(_ context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { return []*influxdb.Label{ {Name: "label_1"}, {Name: "label_2"}, }, nil } svc := newTestService(WithBucketSVC(bktSVC), WithLabelSVC(labelSVC)) resourcesToClone := []ResourceToClone{ { Kind: KindBucket, ID: 10, }, { Kind: KindBucket, ID: 20, }, } bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { bkts := []*influxdb.Bucket{} for _, r := range resourcesToClone { bkts = append(bkts, &influxdb.Bucket{ID: r.ID, Name: strconv.Itoa(int(r.ID)), Type: influxdb.BucketTypeUser}) } return bkts, len(bkts), nil } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) sum := newTemplate.Summary() bkts := sum.Buckets sort.Slice(bkts, func(i, j int) bool { return bkts[i].Name < bkts[j].Name }) require.Len(t, bkts, 2) for i, actual := range bkts { sortLabelsByName(actual.LabelAssociations) assert.Equal(t, strconv.Itoa((i+1)*10), actual.Name) require.Len(t, actual.LabelAssociations, 2) assert.Equal(t, "label_1", actual.LabelAssociations[0].Name) assert.Equal(t, "label_2", actual.LabelAssociations[1].Name) } labels := sum.Labels sortLabelsByName(labels) require.Len(t, labels, 2) assert.Equal(t, "label_1", labels[0].Name) assert.Equal(t, "label_2", labels[1].Name) }) t.Run("labels do not fetch associations", func(t *testing.T) { labelSVC := mock.NewLabelService() labelSVC.FindLabelByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Label, error) { return &influxdb.Label{ID: id, Name: "label_1"}, nil } labelSVC.FindResourceLabelsFn = func(_ context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { return nil, errors.New("should not get here") } svc := newTestService(WithLabelSVC(labelSVC)) resToClone := ResourceToClone{ Kind: KindLabel, ID: 1, } template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) require.NoError(t, err) newTemplate := encodeAndDecode(t, template) labels := newTemplate.Summary().Labels require.Len(t, labels, 1) assert.Equal(t, "label_1", labels[0].Name) }) }) }) t.Run("with org id", func(t *testing.T) { orgID := platform.ID(9000) bkt := &influxdb.Bucket{ID: 1, Name: "bucket"} bktSVC := mock.NewBucketService() bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { if (f.ID != nil && *f.ID != bkt.ID) && (f.OrganizationID == nil || *f.OrganizationID != orgID) { return nil, 0, errors.New("not suppose to get here") } return []*influxdb.Bucket{bkt}, 1, nil } bktSVC.FindBucketByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Bucket, error) { if id != 1 { return nil, errors.New("wrong id") } return bkt, nil } checkSVC := mock.NewCheckService() expectedCheck := &icheck.Deadman{ Base: newThresholdBase(1), TimeSince: mustDuration(t, time.Hour), StaleTime: mustDuration(t, 5*time.Hour), ReportZero: true, Level: notification.Critical, } checkSVC.FindChecksFn = func(ctx context.Context, f influxdb.CheckFilter, _ ...influxdb.FindOptions) ([]influxdb.Check, int, error) { if (f.ID != nil && *f.ID != expectedCheck.GetID()) && (f.OrgID == nil || *f.OrgID != orgID) { return nil, 0, errors.New("not suppose to get here") } return []influxdb.Check{expectedCheck}, 1, nil } checkSVC.FindCheckByIDFn = func(ctx context.Context, id platform.ID) (influxdb.Check, error) { return expectedCheck, nil } dash := &influxdb.Dashboard{ ID: 2, Name: "dashboard", Cells: []*influxdb.Cell{}, } dashSVC := mock.NewDashboardService() dashSVC.FindDashboardsF = func(_ context.Context, f influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { if (f.IDs != nil && len(f.IDs) > 0 && f.IDs[0] != nil && *f.IDs[0] != dash.ID) && (f.OrganizationID == nil || *f.OrganizationID != orgID) { return nil, 0, errors.New("not suppose to get here") } return []*influxdb.Dashboard{dash}, 1, nil } dashSVC.FindDashboardByIDF = func(_ context.Context, id platform.ID) (*influxdb.Dashboard, error) { if id != 2 { return nil, errors.New("wrong id") } return dash, nil } notificationEndpoint := &endpoint.HTTP{ Base: endpoint.Base{ ID: newTestIDPtr(2), Name: "http", }, URL: "http://example.com/id", Username: influxdb.SecretField{Key: "2-username"}, Password: influxdb.SecretField{Key: "2-password"}, AuthMethod: "basic", Method: "POST", } endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, f influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { return []influxdb.NotificationEndpoint{notificationEndpoint}, 1, nil } endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationEndpoint, error) { return notificationEndpoint, nil } expectedRule := &rule.HTTP{ Base: rule.Base{ ID: 12, Name: "rule_0", EndpointID: 2, Every: mustDuration(t, time.Minute), StatusRules: []notification.StatusRule{{CurrentLevel: notification.Critical}}, }, } ruleSVC := mock.NewNotificationRuleStore() ruleSVC.FindNotificationRulesF = func(ctx context.Context, f influxdb.NotificationRuleFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationRule, int, error) { out := []influxdb.NotificationRule{expectedRule} return out, len(out), nil } ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id platform.ID) (influxdb.NotificationRule, error) { return expectedRule, nil } labelSVC := mock.NewLabelService() labelSVC.FindLabelsFn = func(_ context.Context, f influxdb.LabelFilter) ([]*influxdb.Label, error) { if f.OrgID == nil || *f.OrgID != orgID { return nil, errors.New("not suppose to get here") } return []*influxdb.Label{{ID: 3, Name: "label"}}, nil } labelSVC.FindLabelByIDFn = func(_ context.Context, id platform.ID) (*influxdb.Label, error) { if id != 3 { return nil, errors.New("wrong id") } return &influxdb.Label{ID: 3, Name: "label"}, nil } taskSVC := mock.NewTaskService() taskSVC.FindTasksFn = func(ctx context.Context, f taskmodel.TaskFilter) ([]*taskmodel.Task, int, error) { if f.After != nil { return nil, 0, nil } return []*taskmodel.Task{ {ID: 31, Type: taskmodel.TaskSystemType}, {ID: expectedCheck.TaskID, Type: taskmodel.TaskSystemType}, // this one should be ignored in the return {ID: expectedRule.TaskID, Type: taskmodel.TaskSystemType}, // this one should be ignored in the return as well {ID: 99}, // this one should be skipped since it is not a system task }, 3, nil } taskSVC.FindTaskByIDFn = func(ctx context.Context, id platform.ID) (*taskmodel.Task, error) { if id != 31 { return nil, errors.New("wrong id: " + id.String()) } return &taskmodel.Task{ ID: id, Name: "task_0", Every: time.Minute.String(), Offset: 10 * time.Second, Type: taskmodel.TaskSystemType, Flux: `option task = { name: "larry" } from(bucket: "rucket") |> yield()`, }, nil } varSVC := mock.NewVariableService() varSVC.FindVariablesF = func(_ context.Context, f influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { if f.OrganizationID == nil || *f.OrganizationID != orgID { return nil, errors.New("not suppose to get here") } return []*influxdb.Variable{{ID: 4, Name: "variable"}}, nil } varSVC.FindVariableByIDF = func(_ context.Context, id platform.ID) (*influxdb.Variable, error) { if id != 4 { return nil, errors.New("wrong id") } return &influxdb.Variable{ID: 4, Name: "variable"}, nil } svc := newTestService( WithBucketSVC(bktSVC), WithCheckSVC(checkSVC), WithDashboardSVC(dashSVC), WithLabelSVC(labelSVC), WithNotificationEndpointSVC(endpointSVC), WithNotificationRuleSVC(ruleSVC), WithTaskSVC(taskSVC), WithVariableSVC(varSVC), ) template, err := svc.Export( context.TODO(), ExportWithAllOrgResources(ExportByOrgIDOpt{ OrgID: orgID, }), ) require.NoError(t, err) summary := template.Summary() bkts := summary.Buckets require.Len(t, bkts, 1) assert.Equal(t, "bucket", bkts[0].Name) checks := summary.Checks require.Len(t, checks, 1) assert.Equal(t, expectedCheck.Name, checks[0].Check.GetName()) dashs := summary.Dashboards require.Len(t, dashs, 1) assert.Equal(t, "dashboard", dashs[0].Name) labels := summary.Labels require.Len(t, labels, 1) assert.Equal(t, "label", labels[0].Name) endpoints := summary.NotificationEndpoints require.Len(t, endpoints, 1) assert.Equal(t, "http", endpoints[0].NotificationEndpoint.GetName()) rules := summary.NotificationRules require.Len(t, rules, 1) assert.Equal(t, expectedRule.Name, rules[0].Name) assert.NotEmpty(t, rules[0].EndpointMetaName) require.Len(t, summary.Tasks, 1) task1 := summary.Tasks[0] assert.Equal(t, "task_0", task1.Name) vars := summary.Variables require.Len(t, vars, 1) assert.Equal(t, "variable", vars[0].Name) }) }) t.Run("InitStack", func(t *testing.T) { safeCreateFn := func(ctx context.Context, stack Stack) error { return nil } type createFn func(ctx context.Context, stack Stack) error newFakeStore := func(fn createFn) *fakeStore { return &fakeStore{ createFn: fn, } } now := time.Time{}.Add(10 * 24 * time.Hour) t.Run("when store call is successful", func(t *testing.T) { svc := newTestService( WithIDGenerator(newFakeIDGen(3)), WithTimeGenerator(newTimeGen(now)), WithStore(newFakeStore(safeCreateFn)), ) stack, err := svc.InitStack(context.Background(), 9000, StackCreate{OrgID: 3333}) require.NoError(t, err) assert.Equal(t, platform.ID(3), stack.ID) assert.Equal(t, now, stack.CreatedAt) assert.Equal(t, now, stack.LatestEvent().UpdatedAt) }) t.Run("handles unexpected error paths", func(t *testing.T) { tests := []struct { name string expectedErrCode string store func() *fakeStore orgSVC func() influxdb.OrganizationService }{ { name: "unexpected store err", expectedErrCode: errors2.EInternal, store: func() *fakeStore { return newFakeStore(func(ctx context.Context, stack Stack) error { return errors.New("unexpected error") }) }, }, { name: "unexpected conflict store err", expectedErrCode: errors2.EInternal, store: func() *fakeStore { return newFakeStore(func(ctx context.Context, stack Stack) error { return &errors2.Error{Code: errors2.EConflict} }) }, }, { name: "org does not exist produces conflict error", expectedErrCode: errors2.EConflict, store: func() *fakeStore { return newFakeStore(safeCreateFn) }, orgSVC: func() influxdb.OrganizationService { orgSVC := mock.NewOrganizationService() orgSVC.FindOrganizationByIDF = func(ctx context.Context, id platform.ID) (*influxdb.Organization, error) { return nil, &errors2.Error{Code: errors2.ENotFound} } return orgSVC }, }, } for _, tt := range tests { fn := func(t *testing.T) { var orgSVC influxdb.OrganizationService = mock.NewOrganizationService() if tt.orgSVC != nil { orgSVC = tt.orgSVC() } svc := newTestService( WithIDGenerator(newFakeIDGen(3)), WithTimeGenerator(newTimeGen(now)), WithStore(tt.store()), WithOrganizationService(orgSVC), ) _, err := svc.InitStack(context.Background(), 9000, StackCreate{OrgID: 3333}) require.Error(t, err) assert.Equal(t, tt.expectedErrCode, errors2.ErrorCode(err)) } t.Run(tt.name, fn) } }) t.Run("jsonnet template url", func(t *testing.T) { tests := []struct { name string create StackCreate expectedErrCode string }{ // always valid { name: "no templates", create: StackCreate{OrgID: 3333}, }, { name: "one json template", create: StackCreate{ OrgID: 3333, TemplateURLs: []string{"http://fake/some.json"}, }, }, { name: "one yaml template", create: StackCreate{ OrgID: 3333, TemplateURLs: []string{"http://fake/some.yaml"}, }, }, { name: "multiple templates", create: StackCreate{ OrgID: 3333, TemplateURLs: []string{ "http://fake/some.yaml", "http://fake/some.json", "http://fake/other.yaml", }, }, }, // invalid { name: "one jsonnet template", create: StackCreate{ OrgID: 3333, TemplateURLs: []string{"http://fake/some.jsonnet"}, }, expectedErrCode: "unprocessable entity", }, { name: "multiple with one jsonnet template", create: StackCreate{ OrgID: 3333, TemplateURLs: []string{ "http://fake/some.json", "http://fake/some.jsonnet", "http://fake/some.yaml", }, }, expectedErrCode: "unprocessable entity", }, { name: "one weird jsonnet template", create: StackCreate{ OrgID: 3333, TemplateURLs: []string{"http://fake/some.%6asonnet"}, }, expectedErrCode: "unprocessable entity", }, } svc := newTestService( WithIDGenerator(newFakeIDGen(3)), WithTimeGenerator(newTimeGen(now)), WithStore(newFakeStore(safeCreateFn)), ) for _, tt := range tests { ctx := context.Background() stack, err := svc.InitStack(ctx, 9000, tt.create) if tt.expectedErrCode == "" { require.NoError(t, err) assert.Equal(t, platform.ID(3), stack.ID) assert.Equal(t, platform.ID(3333), stack.OrgID) assert.Equal(t, now, stack.CreatedAt) assert.Equal(t, now, stack.LatestEvent().UpdatedAt) } else { require.Error(t, err) assert.Equal(t, tt.expectedErrCode, errors2.ErrorCode(err)) assert.Equal(t, Stack{}, stack) } } }) }) t.Run("UpdateStack", func(t *testing.T) { now := time.Time{}.Add(10 * 24 * time.Hour) t.Run("when updating valid stack", func(t *testing.T) { tests := []struct { name string input StackUpdate expected StackEvent }{ { name: "update nothing", input: StackUpdate{}, expected: StackEvent{ EventType: StackEventUpdate, UpdatedAt: now, }, }, { name: "update name", input: StackUpdate{ Name: strPtr("name"), }, expected: StackEvent{ EventType: StackEventUpdate, Name: "name", UpdatedAt: now, }, }, { name: "update desc", input: StackUpdate{ Description: strPtr("desc"), }, expected: StackEvent{ EventType: StackEventUpdate, Description: "desc", UpdatedAt: now, }, }, { name: "update URLs", input: StackUpdate{ TemplateURLs: []string{"http://example.com"}, }, expected: StackEvent{ EventType: StackEventUpdate, TemplateURLs: []string{"http://example.com"}, UpdatedAt: now, }, }, { name: "update first 3", input: StackUpdate{ Name: strPtr("name"), Description: strPtr("desc"), TemplateURLs: []string{"http://example.com"}, }, expected: StackEvent{ EventType: StackEventUpdate, Name: "name", Description: "desc", TemplateURLs: []string{"http://example.com"}, UpdatedAt: now, }, }, { name: "update with metaname collisions", input: StackUpdate{ Name: strPtr("name"), Description: strPtr("desc"), TemplateURLs: []string{"http://example.com"}, AdditionalResources: []StackAdditionalResource{ { APIVersion: APIVersion, ID: 1, Kind: KindLabel, MetaName: "meta-label", }, { APIVersion: APIVersion, ID: 2, Kind: KindLabel, MetaName: "meta-label", }, }, }, expected: StackEvent{ EventType: StackEventUpdate, Name: "name", Description: "desc", TemplateURLs: []string{"http://example.com"}, Resources: []StackResource{ { APIVersion: APIVersion, ID: 2, Kind: KindLabel, MetaName: "collision-1-" + platform.ID(333).String()[10:], }, { APIVersion: APIVersion, ID: 1, Kind: KindLabel, MetaName: "meta-label", }, }, UpdatedAt: now, }, }, { name: "update all", input: StackUpdate{ Name: strPtr("name"), Description: strPtr("desc"), TemplateURLs: []string{"http://example.com"}, AdditionalResources: []StackAdditionalResource{ { APIVersion: APIVersion, ID: 1, Kind: KindLabel, MetaName: "meta-label", }, }, }, expected: StackEvent{ EventType: StackEventUpdate, Name: "name", Description: "desc", TemplateURLs: []string{"http://example.com"}, Resources: []StackResource{ { APIVersion: APIVersion, ID: 1, Kind: KindLabel, MetaName: "meta-label", }, }, UpdatedAt: now, }, }, } for _, tt := range tests { fn := func(t *testing.T) { var collisions int nameGenFn := func() string { collisions++ return "collision-" + strconv.Itoa(collisions) } svc := newTestService( WithIDGenerator(mock.IDGenerator{ IDFn: func() platform.ID { return 333 }, }), withNameGen(nameGenFn), WithTimeGenerator(newTimeGen(now)), WithStore(&fakeStore{ readFn: func(ctx context.Context, id platform.ID) (Stack, error) { if id != 33 { return Stack{}, errors.New("wrong id: " + id.String()) } return Stack{ID: id, OrgID: 3}, nil }, updateFn: func(ctx context.Context, stack Stack) error { return nil }, }), ) tt.input.ID = 33 stack, err := svc.UpdateStack(context.Background(), tt.input) require.NoError(t, err) assert.Equal(t, platform.ID(33), stack.ID) assert.Equal(t, platform.ID(3), stack.OrgID) assert.Zero(t, stack.CreatedAt) // should always zero value in these tests assert.Equal(t, tt.expected, stack.LatestEvent()) } t.Run(tt.name, fn) } }) t.Run("jsonnet template url", func(t *testing.T) { tests := []struct { name string update StackUpdate expectedErrCode string }{ // always valid { name: "no templates", update: StackUpdate{ID: 3333}, }, { name: "one json template", update: StackUpdate{ ID: 3333, TemplateURLs: []string{"http://fake/some.json"}, }, }, { name: "one yaml template", update: StackUpdate{ ID: 3333, TemplateURLs: []string{"http://fake/some.yaml"}, }, }, { name: "multiple templates", update: StackUpdate{ ID: 3333, TemplateURLs: []string{ "http://fake/some.yaml", "http://fake/some.json", "http://fake/other.yaml", }, }, }, // invalid { name: "one jsonnet template", update: StackUpdate{ ID: 3333, TemplateURLs: []string{"http://fake/some.jsonnet"}, }, expectedErrCode: "unprocessable entity", }, { name: "multiple with one jsonnet template", update: StackUpdate{ ID: 3333, TemplateURLs: []string{ "http://fake/some.json", "http://fake/some.jsonnet", "http://fake/some.yaml", }, }, expectedErrCode: "unprocessable entity", }, { name: "one weird jsonnet template", update: StackUpdate{ ID: 3333, TemplateURLs: []string{"http://fake/some.%6asonnet"}, }, expectedErrCode: "unprocessable entity", }, } for _, tt := range tests { svc := newTestService( WithIDGenerator(mock.IDGenerator{ IDFn: func() platform.ID { return 333 }, }), WithTimeGenerator(newTimeGen(now)), WithStore(&fakeStore{ readFn: func(ctx context.Context, id platform.ID) (Stack, error) { return Stack{ID: id, OrgID: 3}, nil }, updateFn: func(ctx context.Context, stack Stack) error { return nil }, }), ) ctx := context.Background() stack, err := svc.UpdateStack(ctx, tt.update) if tt.expectedErrCode == "" { require.NoError(t, err) assert.Equal(t, platform.ID(3333), stack.ID) assert.Equal(t, platform.ID(3), stack.OrgID) assert.Equal(t, now, stack.LatestEvent().UpdatedAt) } else { require.Error(t, err) assert.Equal(t, tt.expectedErrCode, errors2.ErrorCode(err)) assert.Equal(t, Stack{}, stack) } } }) }) } func Test_normalizeRemoteSources(t *testing.T) { tests := []struct { name string input []string expected []url.URL }{ { name: "no urls provided", input: []string{"byte stream", "string", ""}, expected: nil, }, { name: "skips valid file url", input: []string{"file:///example.com"}, expected: nil, }, { name: "valid http url provided", input: []string{"http://example.com"}, expected: []url.URL{parseURL(t, "http://example.com")}, }, { name: "valid https url provided", input: []string{"https://example.com"}, expected: []url.URL{parseURL(t, "https://example.com")}, }, { name: "converts raw github user url to base github", input: []string{"https://raw.githubusercontent.com/influxdata/community-templates/master/github/github.yml"}, expected: []url.URL{ parseURL(t, "https://github.com/influxdata/community-templates/blob/master/github/github.yml"), }, }, { name: "passes base github link unchanged", input: []string{"https://github.com/influxdata/community-templates/blob/master/github/github.yml"}, expected: []url.URL{ parseURL(t, "https://github.com/influxdata/community-templates/blob/master/github/github.yml"), }, }, } for _, tt := range tests { fn := func(t *testing.T) { actual := normalizeRemoteSources(tt.input) require.Len(t, actual, len(tt.expected)) for i, expected := range tt.expected { assert.Equal(t, expected.String(), actual[i].String()) } } t.Run(tt.name, fn) } } func newTestIDPtr(i int) *platform.ID { id := platform.ID(i) return &id } func levelPtr(l notification.CheckLevel) *notification.CheckLevel { return &l } type fakeStore struct { createFn func(ctx context.Context, stack Stack) error deleteFn func(ctx context.Context, id platform.ID) error readFn func(ctx context.Context, id platform.ID) (Stack, error) updateFn func(ctx context.Context, stack Stack) error } var _ Store = (*fakeStore)(nil) func (s *fakeStore) CreateStack(ctx context.Context, stack Stack) error { if s.createFn != nil { return s.createFn(ctx, stack) } panic("not implemented") } func (s *fakeStore) ListStacks(ctx context.Context, orgID platform.ID, f ListFilter) ([]Stack, error) { panic("not implemented") } func (s *fakeStore) ReadStackByID(ctx context.Context, id platform.ID) (Stack, error) { if s.readFn != nil { return s.readFn(ctx, id) } panic("not implemented") } func (s *fakeStore) UpdateStack(ctx context.Context, stack Stack) error { if s.updateFn != nil { return s.updateFn(ctx, stack) } panic("not implemented") } func (s *fakeStore) DeleteStack(ctx context.Context, id platform.ID) error { if s.deleteFn != nil { return s.deleteFn(ctx, id) } panic("not implemented") } type fakeIDGen func() platform.ID func newFakeIDGen(id platform.ID) fakeIDGen { return func() platform.ID { return id } } func (f fakeIDGen) ID() platform.ID { return f() } type fakeTimeGen func() time.Time func newTimeGen(t time.Time) fakeTimeGen { return func() time.Time { return t } } func (t fakeTimeGen) Now() time.Time { return t() } func parseURL(t *testing.T, rawAddr string) url.URL { t.Helper() u, err := url.Parse(rawAddr) require.NoError(t, err) return *u }