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/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/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 influxdb.ID) error { return nil }, readFn: func(ctx context.Context, id influxdb.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)) } return NewService(applyOpts...) } t.Run("DryRun", func(t *testing.T) { type dryRunTestFields struct { path string kinds []Kind skipResources []ActionSkipResource assertFn func(*testing.T, PkgImpactSummary) } 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, pkg *Pkg) { 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(), influxdb.ID(100), 0, append(tt.applyOpts, ApplyWithPkg(pkg))..., ) 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, pkg *Pkg) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(_ context.Context, orgID influxdb.ID, name string) (*influxdb.Bucket, error) { if name != "rucket-11" { return nil, errors.New("not found") } return &influxdb.Bucket{ ID: influxdb.ID(1), OrgID: orgID, Name: name, Description: "old desc", RetentionPeriod: 30 * time.Hour, }, nil } svc := newTestService(WithBucketSVC(fakeBktSVC)) impact, err := svc.DryRun(context.TODO(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) require.NoError(t, err) require.Len(t, impact.Diff.Buckets, 2) expected := DiffBucket{ DiffIdentifier: DiffIdentifier{ ID: SafeID(1), StateStatus: StateStatusExists, PkgName: "rucket-11", }, 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, pkg *Pkg) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(_ context.Context, orgID influxdb.ID, name string) (*influxdb.Bucket, error) { return nil, errors.New("not found") } svc := newTestService(WithBucketSVC(fakeBktSVC)) impact, err := svc.DryRun(context.TODO(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) require.NoError(t, err) require.Len(t, impact.Diff.Buckets, 2) expected := DiffBucket{ DiffIdentifier: DiffIdentifier{ PkgName: "rucket-11", StateStatus: StateStatusNew, }, 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 PkgImpactSummary) { 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, pkg *Pkg) { fakeCheckSVC := mock.NewCheckService() id := influxdb.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(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) 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.PkgName) assert.Zero(t, check0.ID) assert.Nil(t, check0.Old) check1 := checks[1] assert.False(t, check1.IsNew()) assert.Equal(t, "check-1", check1.PkgName) 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 PkgImpactSummary) { 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 PkgImpactSummary) { 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, pkg *Pkg) { fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { return []*influxdb.Label{ { ID: influxdb.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(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) require.NoError(t, err) require.Len(t, impact.Diff.Labels, 3) expected := DiffLabel{ DiffIdentifier: DiffIdentifier{ ID: SafeID(1), StateStatus: StateStatusExists, PkgName: "label-1", }, 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.PkgName = "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, pkg *Pkg) { 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(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) require.NoError(t, err) labels := impact.Diff.Labels require.Len(t, labels, 3) expected := DiffLabel{ DiffIdentifier: DiffIdentifier{ PkgName: "label-1", StateStatus: StateStatusNew, }, New: DiffLabelValues{ Name: "label-1", Color: "#FFFFFF", Description: "label 1 description", }, } assert.Contains(t, labels, expected) expected.PkgName = "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 PkgImpactSummary) { 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, pkg *Pkg) { fakeEndpointSVC := mock.NewNotificationEndpointService() id := influxdb.ID(1) existing := &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "http-none-auth-notification-endpoint", Description: "old desc", Status: influxdb.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(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) 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, PkgName: "http-none-auth-notification-endpoint", StateStatus: StateStatusExists, }, 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: influxdb.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 PkgImpactSummary) { 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, pkg *Pkg) { fakeEndpointSVC := mock.NewNotificationEndpointService() id := influxdb.ID(1) existing := &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, // This name here matches the endpoint identified in the pkg notification rule Name: "endpoint-0", Description: "old desc", Status: influxdb.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(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) 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 PkgImpactSummary) { 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, pkg *Pkg) { fakeSecretSVC := mock.NewSecretService() fakeSecretSVC.GetSecretKeysFn = func(ctx context.Context, orgID influxdb.ID) ([]string, error) { return []string{"rando-1", "rando-2"}, nil } svc := newTestService(WithSecretSVC(fakeSecretSVC)) impact, err := svc.DryRun(context.TODO(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) 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 PkgImpactSummary) { 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 PkgImpactSummary) { 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, pkg *Pkg) { fakeVarSVC := mock.NewVariableService() fakeVarSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, opts ...influxdb.FindOptions) ([]*influxdb.Variable, error) { return []*influxdb.Variable{ { ID: influxdb.ID(1), Name: "var-const-3", Description: "old desc", }, }, nil } svc := newTestService(WithVariableSVC(fakeVarSVC)) impact, err := svc.DryRun(context.TODO(), influxdb.ID(100), 0, ApplyWithPkg(pkg)) require.NoError(t, err) variables := impact.Diff.Variables require.Len(t, variables, 4) expected := DiffVariable{ DiffIdentifier: DiffIdentifier{ ID: 1, PkgName: "var-const-3", StateStatus: StateStatusExists, }, 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 PkgName: "var-map-4", StateStatus: StateStatusNew, }, 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 PkgImpactSummary) { require.Empty(t, impact.Diff.Variables) }, }) }) }) }) t.Run("Apply", func(t *testing.T) { t.Run("buckets", func(t *testing.T) { t.Run("successfully creates pkg of buckets", func(t *testing.T) { testfileRunner(t, "testdata/bucket.yml", func(t *testing.T, pkg *Pkg) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.CreateBucketFn = func(_ context.Context, b *influxdb.Bucket) error { b.ID = influxdb.ID(b.RetentionPeriod) return nil } fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.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 influxdb.ID, upd influxdb.BucketUpdate) (*influxdb.Bucket, error) { return &influxdb.Bucket{ID: id}, nil } svc := newTestService(WithBucketSVC(fakeBktSVC)) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Buckets, 2) expected := SummaryBucket{ ID: SafeID(time.Hour), OrgID: SafeID(orgID), PkgName: "rucket-11", Name: "rucket-11", Description: "bucket 1 description", RetentionPeriod: time.Hour, LabelAssociations: []SummaryLabel{}, EnvReferences: []SummaryReference{}, } 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, pkg *Pkg) { orgID := influxdb.ID(9000) fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(ctx context.Context, oid influxdb.ID, name string) (*influxdb.Bucket, error) { if orgID != oid { return nil, errors.New("invalid org id") } id := influxdb.ID(3) if name == "display name" { id = 4 name = "rucket-22" } if bkt, ok := pkg.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 influxdb.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, ApplyWithPkg(pkg)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Buckets, 2) expected := SummaryBucket{ ID: SafeID(3), OrgID: SafeID(orgID), PkgName: "rucket-11", Name: "rucket-11", Description: "bucket 1 description", RetentionPeriod: time.Hour, LabelAssociations: []SummaryLabel{}, EnvReferences: []SummaryReference{}, } 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, pkg *Pkg) { fakeBktSVC := mock.NewBucketService() fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.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 := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) require.Error(t, err) assert.GreaterOrEqual(t, fakeBktSVC.DeleteBucketCalls.Count(), 1) }) }) }) t.Run("checks", func(t *testing.T) { t.Run("successfully creates pkg of checks", func(t *testing.T) { testfileRunner(t, "testdata/checks.yml", func(t *testing.T, pkg *Pkg) { fakeCheckSVC := mock.NewCheckService() fakeCheckSVC.CreateCheckFn = func(ctx context.Context, c influxdb.CheckCreate, id influxdb.ID) error { c.SetID(influxdb.ID(fakeCheckSVC.CreateCheckCalls.Count() + 1)) return nil } svc := newTestService(WithCheckSVC(fakeCheckSVC)) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { fakeCheckSVC := mock.NewCheckService() fakeCheckSVC.CreateCheckFn = func(ctx context.Context, c influxdb.CheckCreate, id influxdb.ID) error { c.SetID(influxdb.ID(fakeCheckSVC.CreateCheckCalls.Count() + 1)) if fakeCheckSVC.CreateCheckCalls.Count() == 1 { return errors.New("hit that kill count") } return nil } svc := newTestService(WithCheckSVC(fakeCheckSVC)) orgID := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) require.Error(t, err) assert.GreaterOrEqual(t, fakeCheckSVC.DeleteCheckCalls.Count(), 1) }) }) }) t.Run("labels", func(t *testing.T) { t.Run("successfully creates pkg of labels", func(t *testing.T) { testfileRunner(t, "testdata/label.json", func(t *testing.T, pkg *Pkg) { 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 = influxdb.ID(i) return nil } svc := newTestService(WithLabelSVC(fakeLabelSVC)) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { 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 := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { orgID := influxdb.ID(9000) stubExisting := func(name string, id influxdb.ID) *influxdb.Label { pkgLabel := pkg.mLabels[name] return &influxdb.Label{ // makes all pkg changes same as they are on the existing ID: id, OrgID: orgID, Name: pkgLabel.Name(), Properties: map[string]string{ "color": pkgLabel.Color, "description": pkgLabel.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 := influxdb.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 influxdb.ID, l influxdb.LabelUpdate) (*influxdb.Label, error) { if id == influxdb.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, ApplyWithPkg(pkg)) 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, pkg *Pkg) { fakeDashSVC := mock.NewDashboardService() fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error { d.ID = influxdb.ID(1) return nil } fakeDashSVC.UpdateDashboardCellViewF = func(ctx context.Context, dID influxdb.ID, cID influxdb.ID, upd influxdb.ViewUpdate) (*influxdb.View, error) { return &influxdb.View{}, nil } svc := newTestService(WithDashboardSVC(fakeDashSVC)) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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.PkgName) 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.PkgName) 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, pkg *Pkg) { 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 = influxdb.ID(1) return nil } deletedDashs := make(map[influxdb.ID]bool) fakeDashSVC.DeleteDashboardF = func(_ context.Context, id influxdb.ID) error { deletedDashs[id] = true return nil } svc := newTestService(WithDashboardSVC(fakeDashSVC)) orgID := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { t.Helper() fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { l.ID = influxdb.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 := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { t.Helper() fakeLabelSVC := mock.NewLabelService() fakeLabelSVC.CreateLabelFn = func(_ context.Context, l *influxdb.Label) error { l.ID = influxdb.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 := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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 = influxdb.ID(rand.Int()) return nil } fakeBktSVC.FindBucketByNameFn = func(_ context.Context, id influxdb.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 influxdb.ID) error { c.Check.SetID(influxdb.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 = influxdb.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 influxdb.ID) error { nr.SetID(influxdb.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 influxdb.ID) error { nr.SetID(influxdb.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 influxdb.TaskCreate) (*influxdb.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 &influxdb.Task{ ID: influxdb.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, _ influxdb.ID) error { cfg.ID = influxdb.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 = influxdb.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 pkg of endpoints", func(t *testing.T) { testfileRunner(t, "testdata/notification_endpoint.yml", func(t *testing.T, pkg *Pkg) { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID influxdb.ID) error { nr.SetID(influxdb.ID(fakeEndpointSVC.CreateNotificationEndpointCalls.Count() + 1)) return nil } svc := newTestService(WithNotificationEndpointSVC(fakeEndpointSVC)) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID influxdb.ID) error { nr.SetID(influxdb.ID(fakeEndpointSVC.CreateNotificationEndpointCalls.Count() + 1)) if fakeEndpointSVC.CreateNotificationEndpointCalls.Count() == 3 { return errors.New("hit that kill count") } return nil } svc := newTestService(WithNotificationEndpointSVC(fakeEndpointSVC)) orgID := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { fakeEndpointSVC := mock.NewNotificationEndpointService() fakeEndpointSVC.CreateNotificationEndpointF = func(ctx context.Context, nr influxdb.NotificationEndpoint, userID influxdb.ID) error { nr.SetID(influxdb.ID(fakeEndpointSVC.CreateNotificationEndpointCalls.Count() + 1)) return nil } fakeRuleStore := mock.NewNotificationRuleStore() fakeRuleStore.CreateNotificationRuleF = func(ctx context.Context, nr influxdb.NotificationRuleCreate, userID influxdb.ID) error { nr.SetID(influxdb.ID(fakeRuleStore.CreateNotificationRuleCalls.Count() + 1)) return nil } svc := newTestService( WithNotificationEndpointSVC(fakeEndpointSVC), WithNotificationRuleSVC(fakeRuleStore), ) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.NotificationRules, 1) assert.Equal(t, "rule-uuid", sum.NotificationRules[0].PkgName) 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].EndpointPkgName) 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, pkg *Pkg) { fakeRuleStore := mock.NewNotificationRuleStore() fakeRuleStore.CreateNotificationRuleF = func(ctx context.Context, nr influxdb.NotificationRuleCreate, userID influxdb.ID) error { nr.SetID(influxdb.ID(fakeRuleStore.CreateNotificationRuleCalls.Count() + 1)) return nil } fakeRuleStore.DeleteNotificationRuleF = func(ctx context.Context, id influxdb.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 = influxdb.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 := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { orgID := influxdb.ID(9000) fakeTaskSVC := mock.NewTaskService() fakeTaskSVC.CreateTaskFn = func(ctx context.Context, tc influxdb.TaskCreate) (*influxdb.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 &influxdb.Task{ ID: influxdb.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, ApplyWithPkg(pkg)) 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].PkgName) 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].PkgName) 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, pkg *Pkg) { fakeTaskSVC := mock.NewTaskService() fakeTaskSVC.CreateTaskFn = func(ctx context.Context, tc influxdb.TaskCreate) (*influxdb.Task, error) { if fakeTaskSVC.CreateTaskCalls.Count() == 1 { return nil, errors.New("expected error") } return &influxdb.Task{ ID: influxdb.ID(fakeTaskSVC.CreateTaskCalls.Count() + 1), }, nil } svc := newTestService(WithTaskSVC(fakeTaskSVC)) orgID := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { orgID := influxdb.ID(9000) fakeTeleSVC := mock.NewTelegrafConfigStore() fakeTeleSVC.CreateTelegrafConfigF = func(_ context.Context, tc *influxdb.TelegrafConfig, userID influxdb.ID) error { tc.ID = 1 return nil } svc := newTestService(WithTelegrafSVC(fakeTeleSVC)) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { fakeTeleSVC := mock.NewTelegrafConfigStore() fakeTeleSVC.CreateTelegrafConfigF = func(_ context.Context, tc *influxdb.TelegrafConfig, userID influxdb.ID) error { t.Log("called") if fakeTeleSVC.CreateTelegrafConfigCalls.Count() == 1 { return errors.New("limit hit") } tc.ID = influxdb.ID(1) return nil } fakeTeleSVC.DeleteTelegrafConfigF = func(_ context.Context, id influxdb.ID) error { if id != 1 { return errors.New("wrong id here") } return nil } svc := newTestService(WithTelegrafSVC(fakeTeleSVC)) orgID := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) require.Error(t, err) assert.Equal(t, 1, fakeTeleSVC.DeleteTelegrafConfigCalls.Count()) }) }) }) t.Run("variables", func(t *testing.T) { t.Run("successfully creates pkg of variables", func(t *testing.T) { testfileRunner(t, "testdata/variables.yml", func(t *testing.T, pkg *Pkg) { fakeVarSVC := mock.NewVariableService() fakeVarSVC.CreateVariableF = func(_ context.Context, v *influxdb.Variable) error { v.ID = influxdb.ID(fakeVarSVC.CreateVariableCalls.Count() + 1) return nil } svc := newTestService(WithVariableSVC(fakeVarSVC)) orgID := influxdb.ID(9000) impact, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) require.NoError(t, err) sum := impact.Summary require.Len(t, sum.Variables, 4) expected := sum.Variables[0] assert.True(t, expected.ID > 0 && expected.ID < 5) assert.Equal(t, SafeID(orgID), expected.OrgID) assert.Equal(t, "var-const-3", expected.Name) assert.Equal(t, "var-const-3 desc", expected.Description) require.NotNil(t, expected.Arguments) assert.Equal(t, influxdb.VariableConstantValues{"first val"}, expected.Arguments.Values) 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, pkg *Pkg) { 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 := influxdb.ID(9000) _, err := svc.Apply(context.TODO(), orgID, 0, ApplyWithPkg(pkg)) 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, pkg *Pkg) { orgID := influxdb.ID(9000) fakeVarSVC := mock.NewVariableService() fakeVarSVC.FindVariablesF = func(ctx context.Context, f influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { return []*influxdb.Variable{ { // makes all pkg changes same as they are on the existing ID: influxdb.ID(1), OrganizationID: orgID, Name: pkg.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 influxdb.ID, v *influxdb.VariableUpdate) (*influxdb.Variable, error) { if id > influxdb.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, ApplyWithPkg(pkg)) 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("CreatePkg", func(t *testing.T) { newThresholdBase := func(i int) icheck.Base { return icheck.Base{ ID: influxdb.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, pkg *Pkg) *Pkg { t.Helper() b, err := pkg.Encode(EncodingJSON) require.NoError(t, err) newPkg, err := Parse(EncodingJSON, FromReader(bytes.NewReader(b))) require.NoError(t, err) return newPkg } 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 influxdb.ID) (*influxdb.Bucket, error) { if id != expected.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expected, nil } svc := newTestService(WithBucketSVC(bktSVC), WithLabelSVC(mock.NewLabelService())) resToClone := ResourceToClone{ Kind: KindBucket, ID: expected.ID, Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) bkts := newPkg.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) } }) 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 := influxdb.ID(1) tt.expected.SetID(id) checkSVC := mock.NewCheckService() checkSVC.FindCheckByIDFn = func(ctx context.Context, id influxdb.ID) (influxdb.Check, error) { if id != tt.expected.GetID() { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return tt.expected, nil } svc := newTestService(WithCheckSVC(checkSVC)) resToClone := ResourceToClone{ Kind: KindCheck, ID: tt.expected.GetID(), Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) checks := newPkg.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) } }) newQuery := func() influxdb.DashboardQuery { q := influxdb.DashboardQuery{ Text: "from(v.bucket) |> count()", EditMode: "advanced", } // TODO: remove this when issue that forced the builder tag to be here to render in UI. q.BuilderConfig.Tags = append(q.BuilderConfig.Tags, influxdb.NewBuilderTag("_measurement", "filter", "")) return q } 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: "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", YColumn: "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", BinSize: 10, TimeFormat: "", }, }, }, { 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", }, }, }, { 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", YColumn: "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", TimeFormat: "", }, }, }, { 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}, Legend: influxdb.Legend{Type: "type", Orientation: "horizontal"}, Note: "a note", Prefix: "pre", Suffix: "suf", Queries: []influxdb.DashboardQuery{newQuery()}, ShadeBelow: true, ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, XColumn: "x", YColumn: "y", Position: "stacked", }, }, }, { name: "xy", newName: "new name", expectedView: influxdb.View{ ViewContents: influxdb.ViewContents{ Name: "view name", }, Properties: influxdb.XYViewProperties{ Type: influxdb.ViewPropertyTypeXY, Axes: newAxes(), Geom: "step", Legend: influxdb.Legend{Type: "type", Orientation: "horizontal"}, Note: "a note", Queries: []influxdb.DashboardQuery{newQuery()}, ShadeBelow: true, ShowNoteWhenEmpty: true, ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, XColumn: "x", YColumn: "y", Position: "overlaid", TimeFormat: "", }, }, }, { 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 influxdb.ID) (*influxdb.Dashboard, error) { if id != expected.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expected, nil } dashSVC.GetDashboardCellViewF = func(_ context.Context, id influxdb.ID, cID influxdb.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, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) dashs := newPkg.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 influxdb.ID) (*influxdb.Dashboard, error) { return &influxdb.Dashboard{ ID: id, Name: "dash name", Description: "desc", }, nil } svc := newTestService(WithDashboardSVC(dashSVC), WithLabelSVC(mock.NewLabelService())) resourcesToClone := []ResourceToClone{ { Kind: KindDashboard, ID: 1, }, { Kind: KindDashboard, ID: 2, }, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resourcesToClone...)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) dashs := newPkg.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("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 influxdb.ID) (*influxdb.Label, error) { if id != expectedLabel.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expectedLabel, nil } svc := newTestService(WithLabelSVC(labelSVC)) resToClone := ResourceToClone{ Kind: KindLabel, ID: expectedLabel.ID, Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) newLabels := newPkg.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("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: influxdb.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: influxdb.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: influxdb.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: influxdb.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: influxdb.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: influxdb.TaskStatusInactive, }, AuthMethod: "none", Method: "GET", URL: "http://example.com", }, }, } for _, tt := range tests { fn := func(t *testing.T) { id := influxdb.ID(1) tt.expected.SetID(id) endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationEndpoint, error) { if id != tt.expected.GetID() { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return tt.expected, nil } svc := newTestService(WithNotificationEndpointSVC(endpointSVC)) resToClone := ResourceToClone{ Kind: KindNotificationEndpoint, ID: tt.expected.GetID(), Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) endpoints := newPkg.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) } }) t.Run("notification rules", func(t *testing.T) { newRuleBase := func(id int) rule.Base { return rule.Base{ ID: influxdb.ID(id), Name: "old_name", Description: "desc", EndpointID: influxdb.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: influxdb.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: influxdb.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: influxdb.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 influxdb.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 influxdb.ID) (influxdb.NotificationRule, error) { return tt.rule, nil } svc := newTestService( WithNotificationEndpointSVC(endpointSVC), WithNotificationRuleSVC(ruleSVC), ) resToClone := ResourceToClone{ Kind: KindNotificationRule, ID: tt.rule.GetID(), Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.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.EndpointPkgName) 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, pkg.Summary().NotificationEndpoints, 1) actualEndpoint := pkg.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 influxdb.ID) (influxdb.NotificationEndpoint, error) { return &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "endpoint_0", Description: "desc", Status: influxdb.TaskStatusInactive, }, AuthMethod: "none", Method: "GET", URL: "http://example.com", }, nil } ruleSVC := mock.NewNotificationRuleStore() ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id influxdb.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, }, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resourcesToClone...)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.Summary() require.Len(t, sum.NotificationRules, len(resourcesToClone)) expectedSameEndpointName := sum.NotificationRules[0].EndpointPkgName 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.EndpointPkgName) } require.Len(t, sum.NotificationEndpoints, 1) assert.Equal(t, "endpoint_0", sum.NotificationEndpoints[0].NotificationEndpoint.GetName()) }) }) t.Run("tasks", func(t *testing.T) { t.Run("single task exports", func(t *testing.T) { tests := []struct { name string newName string task influxdb.Task }{ { name: "every offset is set", newName: "new name", task: influxdb.Task{ ID: 1, Name: "name_9000", Every: time.Minute.String(), Offset: 10 * time.Second, Type: influxdb.TaskSystemType, Flux: `option task = { name: "larry" } from(bucket: "rucket") |> yield()`, }, }, { name: "cron is set", task: influxdb.Task{ ID: 1, Name: "name_0", Cron: "2 * * * *", Type: influxdb.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 influxdb.ID) (*influxdb.Task, error) { if id != tt.task.ID { return nil, errors.New("wrong id provided: " + id.String()) } return &tt.task, nil } svc := newTestService(WithTaskSVC(taskSVC)) resToClone := ResourceToClone{ Kind: KindTask, ID: tt.task.ID, Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.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 influxdb.ID) (*influxdb.Task, error) { return &influxdb.Task{ ID: id, Type: influxdb.TaskSystemType, Name: "same name", Description: "desc", Status: influxdb.TaskStatusActive, Flux: `from(bucket: "foo")`, Every: "5m0s", }, nil } svc := newTestService(WithTaskSVC(taskSVC)) resourcesToClone := []ResourceToClone{ { Kind: KindTask, ID: 1, }, { Kind: KindTask, ID: 2, }, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resourcesToClone...)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.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("telegraf configs", func(t *testing.T) { t.Run("allows for duplicate telegraf names to be exported", func(t *testing.T) { teleStore := mock.NewTelegrafConfigStore() teleStore.FindTelegrafConfigByIDF = func(ctx context.Context, id influxdb.ID) (*influxdb.TelegrafConfig, error) { return &influxdb.TelegrafConfig{ ID: id, OrgID: 9000, Name: "same name", Description: "desc", Config: "some config string", }, nil } svc := newTestService(WithTelegrafSVC(teleStore)) resourcesToClone := []ResourceToClone{ { Kind: KindTelegraf, ID: 1, }, { Kind: KindTelegraf, ID: 2, }, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resourcesToClone...)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.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("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", Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"val"}, }, }, }, { name: "with new name", newName: "new name", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Arguments: &influxdb.VariableArguments{ Type: "constant", Values: influxdb.VariableConstantValues{"val"}, }, }, }, { name: "with map arg", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Arguments: &influxdb.VariableArguments{ Type: "map", Values: influxdb.VariableMapValues{"k": "v"}, }, }, }, { name: "with query arg", expectedVar: influxdb.Variable{ ID: 1, Name: "old name", Arguments: &influxdb.VariableArguments{ Type: "query", Values: influxdb.VariableQueryValues{ Query: "query", Language: "flux", }, }, }, }, } for _, tt := range tests { fn := func(t *testing.T) { varSVC := mock.NewVariableService() varSVC.FindVariableByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.Variable, error) { if id != tt.expectedVar.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return &tt.expectedVar, nil } svc := newTestService(WithVariableSVC(varSVC), WithLabelSVC(mock.NewLabelService())) resToClone := ResourceToClone{ Kind: KindVariable, ID: tt.expectedVar.ID, Name: tt.newName, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) newVars := newPkg.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.Arguments, actual.Arguments) } 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 influxdb.ID) (*influxdb.Bucket, error) { if id != expected.ID { return nil, errors.New("uh ohhh, wrong id here: " + id.String()) } return expected, 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, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.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 influxdb.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, }, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resourcesToClone...)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) sum := newPkg.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 influxdb.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, } pkg, err := svc.CreatePkg(context.TODO(), CreateWithExistingResources(resToClone)) require.NoError(t, err) newPkg := encodeAndDecode(t, pkg) labels := newPkg.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 := influxdb.ID(9000) bktSVC := mock.NewBucketService() bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { if f.OrganizationID == nil || *f.OrganizationID != orgID { return nil, 0, errors.New("not suppose to get here") } return []*influxdb.Bucket{{ID: 1, Name: "bucket"}}, 1, nil } bktSVC.FindBucketByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Bucket, error) { if id != 1 { return nil, errors.New("wrong id") } return &influxdb.Bucket{ID: 1, Name: "bucket"}, 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.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 influxdb.ID) (influxdb.Check, error) { return expectedCheck, nil } dashSVC := mock.NewDashboardService() dashSVC.FindDashboardsF = func(_ context.Context, f influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { if f.OrganizationID == nil || *f.OrganizationID != orgID { return nil, 0, errors.New("not suppose to get here") } return []*influxdb.Dashboard{{ ID: 2, Name: "dashboard", Cells: []*influxdb.Cell{}, }}, 1, nil } dashSVC.FindDashboardByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.Dashboard, error) { if id != 2 { return nil, errors.New("wrong id") } return &influxdb.Dashboard{ ID: 2, Name: "dashboard", Cells: []*influxdb.Cell{}, }, nil } endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, f influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { id := influxdb.ID(2) endpoints := []influxdb.NotificationEndpoint{ &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "http", }, URL: "http://example.com", Username: influxdb.SecretField{Key: id.String() + "-username"}, Password: influxdb.SecretField{Key: id.String() + "-password"}, AuthMethod: "basic", Method: "POST", }, } return endpoints, len(endpoints), nil } endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationEndpoint, error) { return &endpoint.HTTP{ Base: endpoint.Base{ ID: &id, Name: "http", }, URL: "http://example.com", Username: influxdb.SecretField{Key: id.String() + "-username"}, Password: influxdb.SecretField{Key: id.String() + "-password"}, AuthMethod: "basic", Method: "POST", }, 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 influxdb.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 influxdb.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 influxdb.TaskFilter) ([]*influxdb.Task, int, error) { return []*influxdb.Task{ {ID: 31, Type: influxdb.TaskSystemType}, {ID: expectedCheck.TaskID, Type: influxdb.TaskSystemType}, // this one should be ignored in the return {ID: expectedRule.TaskID, Type: influxdb.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 influxdb.ID) (*influxdb.Task, error) { if id != 31 { return nil, errors.New("wrong id: " + id.String()) } return &influxdb.Task{ ID: id, Name: "task_0", Every: time.Minute.String(), Offset: 10 * time.Second, Type: influxdb.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 influxdb.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), ) pkg, err := svc.CreatePkg( context.TODO(), CreateWithAllOrgResources(CreateByOrgIDOpt{ OrgID: orgID, }), ) require.NoError(t, err) summary := pkg.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].EndpointPkgName) 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, Stack{OrgID: 3333}) require.NoError(t, err) assert.Equal(t, influxdb.ID(3), stack.ID) assert.Equal(t, now, stack.CreatedAt) assert.Equal(t, now, stack.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: influxdb.EInternal, store: func() *fakeStore { return newFakeStore(func(ctx context.Context, stack Stack) error { return errors.New("unexpected error") }) }, }, { name: "unexpected conflict store err", expectedErrCode: influxdb.EInternal, store: func() *fakeStore { return newFakeStore(func(ctx context.Context, stack Stack) error { return &influxdb.Error{Code: influxdb.EConflict} }) }, }, { name: "org does not exist produces conflict error", expectedErrCode: influxdb.EConflict, store: func() *fakeStore { return newFakeStore(safeCreateFn) }, orgSVC: func() influxdb.OrganizationService { orgSVC := mock.NewOrganizationService() orgSVC.FindOrganizationByIDF = func(ctx context.Context, id influxdb.ID) (*influxdb.Organization, error) { return nil, &influxdb.Error{Code: influxdb.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, Stack{OrgID: 3333}) require.Error(t, err) assert.Equal(t, tt.expectedErrCode, influxdb.ErrorCode(err)) } t.Run(tt.name, fn) } }) }) 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 Stack }{ { name: "update nothing", input: StackUpdate{}, expected: Stack{}, }, { name: "update name", input: StackUpdate{ Name: strPtr("name"), }, expected: Stack{ Name: "name", }, }, { name: "update desc", input: StackUpdate{ Description: strPtr("desc"), }, expected: Stack{ Description: "desc", }, }, { name: "update URLs", input: StackUpdate{ URLs: []string{"http://example.com"}, }, expected: Stack{ URLs: []string{"http://example.com"}, }, }, { name: "update all", input: StackUpdate{ Name: strPtr("name"), Description: strPtr("desc"), URLs: []string{"http://example.com"}, }, expected: Stack{ Name: "name", Description: "desc", URLs: []string{"http://example.com"}, }, }, } for _, tt := range tests { fn := func(t *testing.T) { svc := newTestService( WithTimeGenerator(newTimeGen(now)), WithStore(&fakeStore{ readFn: func(ctx context.Context, id influxdb.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, influxdb.ID(33), stack.ID) assert.Equal(t, influxdb.ID(3), stack.OrgID) assert.Zero(t, stack.CreatedAt) // should always zero value in these tests assert.Equal(t, tt.expected.Name, stack.Name) assert.Equal(t, tt.expected.Description, stack.Description) assert.Equal(t, tt.expected.URLs, stack.URLs) assert.Equal(t, now, stack.UpdatedAt) } t.Run(tt.name, fn) } }) }) } 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) *influxdb.ID { id := influxdb.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 influxdb.ID) error readFn func(ctx context.Context, id influxdb.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 influxdb.ID, f ListFilter) ([]Stack, error) { panic("not implemented") } func (s *fakeStore) ReadStackByID(ctx context.Context, id influxdb.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 influxdb.ID) error { if s.deleteFn != nil { return s.deleteFn(ctx, id) } panic("not implemented") } type fakeIDGen func() influxdb.ID func newFakeIDGen(id influxdb.ID) fakeIDGen { return func() influxdb.ID { return id } } func (f fakeIDGen) ID() influxdb.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 }