diff --git a/annotation.go b/annotation.go new file mode 100644 index 0000000000..085b43b27c --- /dev/null +++ b/annotation.go @@ -0,0 +1,376 @@ +package influxdb + +import ( + "context" + "encoding/json" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/influxdata/influxdb/v2/kit/platform" + "github.com/influxdata/influxdb/v2/kit/platform/errors" +) + +var ( + errEmptySummary = &errors.Error{ + Code: errors.EInvalid, + Msg: "summary cannot be empty", + } + errSummaryTooLong = &errors.Error{ + Code: errors.EInvalid, + Msg: "summary must be less than 255 characters", + } + errStreamTagTooLong = &errors.Error{ + Code: errors.EInvalid, + Msg: "stream tag must be less than 255 characters", + } + errStreamNameTooLong = &errors.Error{ + Code: errors.EInvalid, + Msg: "stream name must be less than 255 characters", + } + errStreamDescTooLong = &errors.Error{ + Code: errors.EInvalid, + Msg: "stream description must be less than 1024 characters", + } + errStickerTooLong = &errors.Error{ + Code: errors.EInvalid, + Msg: "stickers must be less than 255 characters", + } + errMsgTooLong = &errors.Error{ + Code: errors.EInvalid, + Msg: "message must be less than 4096 characters", + } + errReversedTimes = &errors.Error{ + Code: errors.EInvalid, + Msg: "start time must come before end time", + } + errMissingStreamName = &errors.Error{ + Code: errors.EInvalid, + Msg: "stream name must be set", + } + errMissingStreamTagOrId = &errors.Error{ + Code: errors.EInvalid, + Msg: "stream tag or id must be set", + } + errMissingEndTime = &errors.Error{ + Code: errors.EInvalid, + Msg: "end time must be set", + } + errMissingStartTime = &errors.Error{ + Code: errors.EInvalid, + Msg: "start time must be set", + } +) + +// Service is the service contract for Annotations +type AnnotationService interface { + // CreateAnnotations creates annotations. + CreateAnnotations(ctx context.Context, orgID platform.ID, create []AnnotationCreate) ([]AnnotationEvent, error) + // ListAnnotations lists all annotations matching the filter. + ListAnnotations(ctx context.Context, orgID platform.ID, filter AnnotationListFilter) (ReadAnnotations, error) + // GetAnnotation gets an annotation by id. + GetAnnotation(ctx context.Context, orgID, id platform.ID) (*AnnotationEvent, error) + // DeleteAnnotations deletes annotations matching the filter. + DeleteAnnotations(ctx context.Context, orgID platform.ID, delete AnnotationDeleteFilter) error + // DeleteAnnotation deletes an annotation by id. + DeleteAnnotation(ctx context.Context, orgID, id platform.ID) error + // UpdateAnnotation updates an annotation. + UpdateAnnotation(ctx context.Context, orgID, id platform.ID, update AnnotationCreate) (*AnnotationEvent, error) + + // ListStreams lists all streams matching the filter. + ListStreams(ctx context.Context, orgID platform.ID, filter StreamListFilter) ([]ReadStream, error) + // CreateOrUpdateStream creates or updates the matching stream by name. + CreateOrUpdateStream(ctx context.Context, orgID platform.ID, stream Stream) (*ReadStream, error) + // UpdateStream updates the stream by the ID. + UpdateStream(ctx context.Context, orgID, id platform.ID, stream Stream) (*ReadStream, error) + // DeleteStream deletes the stream metadata by name. + DeleteStream(ctx context.Context, orgID platform.ID, streamName string) error + // DeleteStreamByID deletes the stream metadata by id. + DeleteStreamByID(ctx context.Context, orgID, id platform.ID) error +} + +// AnnotationEvent contains fields for annotating an event. +type AnnotationEvent struct { + ID platform.ID `json:"id,omitempty"` // ID is the annotation ID. + AnnotationCreate // AnnotationCreate defines the common input/output bits of an annotation. +} + +// AnnotationCreate contains user providable fields for annotating an event. +type AnnotationCreate struct { + StreamTag string `json:"stream,omitempty"` // StreamTag provides a means to logically group a set of annotated events. + Summary string `json:"summary"` // Summary is the only field required to annotate an event. + Message string `json:"message,omitempty"` // Message provides more details about the event being annotated. + Stickers map[string]string `json:"stickers,omitempty"` // Stickers are like tags, but named something obscure to differentiate them from influx tags. They are there to differentiate an annotated event. + EndTime *time.Time `json:"endTime,omitempty"` // EndTime is the time of the event being annotated. Defaults to now if not set. + StartTime *time.Time `json:"startTime,omitempty"` // StartTime is the start time of the event being annotated. Defaults to EndTime if not set. +} + +// StoredAnnotation represents annotation data to be stored in the database. +type StoredAnnotation struct { + ID platform.ID `db:"id"` // ID is the annotation's id. + OrgID platform.ID `db:"org_id"` // OrgID is the annotations's owning organization. + StreamID platform.ID `db:"stream_id"` // StreamID is the id of a stream. + StreamTag string `db:"name"` // StreamTag is the name of a stream (when selecting with join of streams). + Summary string `db:"summary"` // Summary is the summary of the annotated event. + Message string `db:"message"` // Message is a longer description of the annotated event. + Stickers []string `db:"stickers"` // Stickers are additional labels to group annotations by. + Duration string `db:"duration"` // Duration is the time range (with zone) of an annotated event. + Lower string `db:"lower"` // Lower is the time an annotated event beings. + Upper string `db:"upper"` // Upper is the time an annotated event ends. +} + +// Validate validates the creation object. +func (a *AnnotationCreate) Validate(nowFunc func() time.Time) error { + switch s := utf8.RuneCountInString(a.Summary); { + case s <= 0: + return errEmptySummary + case s > 255: + return errSummaryTooLong + } + + switch t := utf8.RuneCountInString(a.StreamTag); { + case t == 0: + a.StreamTag = "default" + case t > 255: + return errStreamTagTooLong + } + + if utf8.RuneCountInString(a.Message) > 4096 { + return errMsgTooLong + } + + for k, v := range a.Stickers { + if utf8.RuneCountInString(k) > 255 || utf8.RuneCountInString(v) > 255 { + return errStickerTooLong + } + } + + now := nowFunc() + if a.EndTime == nil { + a.EndTime = &now + } + + if a.StartTime == nil { + a.StartTime = a.EndTime + } + + if a.EndTime.Before(*(a.StartTime)) { + return errReversedTimes + } + + return nil +} + +// AnnotationDeleteFilter contains fields for deleting an annotated event. +type AnnotationDeleteFilter struct { + StreamTag string `json:"stream,omitempty"` // StreamTag provides a means to logically group a set of annotated events. + StreamID platform.ID `json:"streamID,omitempty"` // StreamID provides a means to logically group a set of annotated events. + Stickers map[string]string `json:"stickers,omitempty"` // Stickers are like tags, but named something obscure to differentiate them from influx tags. They are there to differentiate an annotated event. + EndTime *time.Time `json:"endTime,omitempty"` // EndTime is the time of the event being annotated. Defaults to now if not set. + StartTime *time.Time `json:"startTime,omitempty"` // StartTime is the start time of the event being annotated. Defaults to EndTime if not set. +} + +// Validate validates the deletion object. +func (a *AnnotationDeleteFilter) Validate() error { + var errs []string + + if len(a.StreamTag) == 0 && !a.StreamID.Valid() { + errs = append(errs, errMissingStreamTagOrId.Error()) + } + + if a.EndTime == nil { + errs = append(errs, errMissingEndTime.Error()) + } + + if a.StartTime == nil { + errs = append(errs, errMissingStartTime.Error()) + } + + if len(errs) > 0 { + return &errors.Error{ + Code: errors.EInvalid, + Msg: strings.Join(errs, "; "), + } + } + + if a.EndTime.Before(*(a.StartTime)) { + return errReversedTimes + } + + return nil +} + +var dre = regexp.MustCompile(`stickers\[(.*)\]`) + +// SetStickers sets the stickers from the query parameters. +func (a *AnnotationDeleteFilter) SetStickers(vals map[string][]string) { + if a.Stickers == nil { + a.Stickers = map[string]string{} + } + + for k, v := range vals { + if ss := dre.FindStringSubmatch(k); len(ss) == 2 && len(v) > 0 { + a.Stickers[ss[1]] = v[0] + } + } +} + +// AnnotationList defines the structure of the response when listing annotations. +type AnnotationList struct { + StreamTag string `json:"stream"` + Annotations []ReadAnnotation `json:"annotations"` +} + +// ReadAnnotations allows annotations to be assigned to a stream. +type ReadAnnotations map[string][]ReadAnnotation + +// MarshalJSON allows us to marshal the annotations belonging to a stream properly. +func (s ReadAnnotations) MarshalJSON() ([]byte, error) { + annotationList := []AnnotationList{} + + for k, v := range s { + annotationList = append(annotationList, AnnotationList{ + StreamTag: k, + Annotations: v, + }) + } + + return json.Marshal(annotationList) +} + +// ReadAnnotation defines the simplest form of an annotation to be returned. Essentially, it's AnnotationEvent without stream info. +type ReadAnnotation struct { + ID platform.ID `json:"id"` // ID is the annotation's generated id. + Summary string `json:"summary"` // Summary is the only field required to annotate an event. + Message string `json:"message,omitempty"` // Message provides more details about the event being annotated. + Stickers map[string]string `json:"stickers,omitempty"` // Stickers are like tags, but named something obscure to differentiate them from influx tags. They are there to differentiate an annotated event. + EndTime string `json:"endTime"` // EndTime is the time of the event being annotated. + StartTime string `json:"startTime,omitempty"` // StartTime is the start time of the event being annotated. +} + +// AnnotationListFilter is a selection filter for listing annotations. +type AnnotationListFilter struct { + StickerIncludes map[string]string `json:"stickerIncludes,omitempty"` // StickerIncludes allows the user to filter annotated events based on it's sticker. + StreamIncludes []string `json:"streamIncludes,omitempty"` // StreamIncludes allows the user to filter annotated events by stream. + BasicFilter +} + +// Validate validates the filter. +func (f *AnnotationListFilter) Validate(nowFunc func() time.Time) error { + return f.BasicFilter.Validate(nowFunc) +} + +var re = regexp.MustCompile(`stickerIncludes\[(.*)\]`) + +// SetStickerIncludes sets the stickerIncludes from the query parameters. +func (f *AnnotationListFilter) SetStickerIncludes(vals map[string][]string) { + if f.StickerIncludes == nil { + f.StickerIncludes = map[string]string{} + } + + for k, v := range vals { + if ss := re.FindStringSubmatch(k); len(ss) == 2 && len(v) > 0 { + f.StickerIncludes[ss[1]] = v[0] + } + } +} + +// StreamListFilter is a selection filter for listing streams. Streams are not considered first class resources, but depend on an annotation using them. +type StreamListFilter struct { + StreamIncludes []string `json:"streamIncludes,omitempty"` // StreamIncludes allows the user to filter streams returned. + BasicFilter +} + +// Validate validates the filter. +func (f *StreamListFilter) Validate(nowFunc func() time.Time) error { + return f.BasicFilter.Validate(nowFunc) +} + +// Stream defines the stream metadata. Used in create and update requests/responses. Delete requests will only require stream name. +type Stream struct { + Name string `json:"stream"` // Name is the name of a stream. + Description string `json:"description,omitempty"` // Description is more information about a stream. +} + +// ReadStream defines the returned stream. +type ReadStream struct { + ID platform.ID `json:"id" db:"id"` // ID is the id of a stream. + Name string `json:"stream" db:"name"` // Name is the name of a stream. + Description string `json:"description,omitempty" db:"description"` // Description is more information about a stream. + CreatedAt time.Time `json:"createdAt" db:"created_at"` // CreatedAt is a timestamp. + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` // UpdatedAt is a timestamp. +} + +// IsValid validates the stream. +func (s *Stream) Validate(strict bool) error { + switch nameChars := utf8.RuneCountInString(s.Name); { + case nameChars <= 0: + if strict { + return errMissingStreamName + } + s.Name = "default" + case nameChars > 255: + return errStreamNameTooLong + } + + if utf8.RuneCountInString(s.Description) > 1024 { + return errStreamDescTooLong + } + + return nil +} + +// StoredStream represents stream data to be stored in the metadata database. +type StoredStream struct { + ID platform.ID `db:"id"` // ID is the stream's id. + OrgID platform.ID `db:"org_id"` // OrgID is the stream's owning organization. + Name string `db:"name"` // Name is the name of a stream. + Description string `db:"description"` // Description is more information about a stream. + CreatedAt time.Time `db:"created_at"` // CreatedAt is a timestamp. + UpdatedAt time.Time `db:"updated_at"` // UpdatedAt is a timestamp. +} + +// BasicStream defines a stream by name. Used for stream deletes. +type BasicStream struct { + Names []string `json:"stream"` +} + +// IsValid validates the stream is not empty. +func (s BasicStream) IsValid() bool { + if len(s.Names) <= 0 { + return false + } + + for i := range s.Names { + if len(s.Names[i]) <= 0 { + return false + } + } + + return true +} + +// BasicFilter defines common filter options. +type BasicFilter struct { + StartTime *time.Time `json:"startTime,omitempty"` // StartTime is the time the event being annotated started. + EndTime *time.Time `json:"endTime,omitempty"` // EndTime is the time the event being annotated ended. +} + +// Validate validates the basic filter options, setting sane defaults where appropriate. +func (f *BasicFilter) Validate(nowFunc func() time.Time) error { + now := nowFunc().UTC().Truncate(time.Second) + if f.EndTime == nil || f.EndTime.IsZero() { + f.EndTime = &now + } + + if f.StartTime == nil { + f.StartTime = &time.Time{} + } + + if f.EndTime.Before(*(f.StartTime)) { + return errReversedTimes + } + + return nil +} diff --git a/annotation_test.go b/annotation_test.go new file mode 100644 index 0000000000..8e45ef9e2e --- /dev/null +++ b/annotation_test.go @@ -0,0 +1,556 @@ +package influxdb + +import ( + "encoding/json" + "strings" + "testing" + "time" + + "github.com/influxdata/influxdb/v2/kit/platform" + "github.com/influxdata/influxdb/v2/kit/platform/errors" + "github.com/stretchr/testify/require" +) + +var ( + testTime time.Time = time.Now() + testTime2 time.Time = testTime.Add(time.Minute) + + annID, _ = platform.IDFromString("2345678901234567") +) + +func nowFunc() time.Time { + return testTime +} + +func TestAnnotationCreate(t *testing.T) { + type tst struct { + name string + input AnnotationCreate + expected AnnotationCreate + err *errors.Error + } + + tests := []tst{ + { + name: "minimum valid create request", + input: AnnotationCreate{ + Summary: "this is a default annotation", + }, + expected: AnnotationCreate{ + StreamTag: "default", + Summary: "this is a default annotation", + EndTime: &testTime, + StartTime: &testTime, + }, + }, + { + name: "full valid create request", + input: AnnotationCreate{ + StreamTag: "other", + Summary: "this is another annotation", + Message: "This is a much longer description or message to add to the annotation summary", + Stickers: map[string]string{"product": "cloud"}, + EndTime: &testTime2, + StartTime: &testTime, + }, + expected: AnnotationCreate{ + StreamTag: "other", + Summary: "this is another annotation", + Message: "This is a much longer description or message to add to the annotation summary", + Stickers: map[string]string{"product": "cloud"}, + EndTime: &testTime2, + StartTime: &testTime, + }, + }, + { + name: "empty create request", + input: AnnotationCreate{}, + err: errEmptySummary, + }, + { + name: "end time before start create request", + input: AnnotationCreate{ + Summary: "this is a default annotation", + EndTime: &testTime, + StartTime: &testTime2, + }, + err: errReversedTimes, + }, + { + name: "default end time before start create request", + input: AnnotationCreate{ + Summary: "this is a default annotation", + StartTime: &testTime2, + }, + err: errReversedTimes, + }, + { + name: "summary too long", + input: AnnotationCreate{ + Summary: strings.Repeat("a", 256), + }, + err: errSummaryTooLong, + }, + { + name: "message too long", + input: AnnotationCreate{ + Summary: "longTom", + Message: strings.Repeat("a", 4097), + }, + err: errMsgTooLong, + }, + { + name: "stream tag too long", + input: AnnotationCreate{ + Summary: "longTom", + StreamTag: strings.Repeat("a", 256), + }, + err: errStreamTagTooLong, + }, + { + name: "sticker key too long", + input: AnnotationCreate{ + Summary: "longTom", + Stickers: map[string]string{strings.Repeat("1", 256): "val"}, + }, + err: errStickerTooLong, + }, + { + name: "sticker val too long", + input: AnnotationCreate{ + Summary: "longTom", + Stickers: map[string]string{"key": strings.Repeat("1", 256)}, + }, + err: errStickerTooLong, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.input.Validate(nowFunc) + if test.err != nil { + require.Equal(t, test.err, err) + return + } + + require.NoError(t, err) + require.Equal(t, test.expected, test.input) + }) + } +} + +func TestDeleteFilter(t *testing.T) { + type tst struct { + name string + input AnnotationDeleteFilter + expected AnnotationDeleteFilter + err *errors.Error + } + + tests := []tst{ + { + name: "minimum valid delete", + input: AnnotationDeleteFilter{ + StreamTag: "default", + EndTime: &testTime, + StartTime: &testTime, + }, + expected: AnnotationDeleteFilter{ + StreamTag: "default", + EndTime: &testTime, + StartTime: &testTime, + }, + }, + { + name: "full valid delete", + input: AnnotationDeleteFilter{ + StreamTag: "default", + Stickers: map[string]string{"product": "oss"}, + EndTime: &testTime, + StartTime: &testTime, + }, + expected: AnnotationDeleteFilter{ + StreamTag: "default", + Stickers: map[string]string{"product": "oss"}, + EndTime: &testTime, + StartTime: &testTime, + }, + }, + { + name: "missing stream tag", + input: AnnotationDeleteFilter{ + Stickers: map[string]string{"product": "oss"}, + EndTime: &testTime, + StartTime: &testTime, + }, + err: errMissingStreamTagOrId, + }, + { + name: "missing start time", + input: AnnotationDeleteFilter{ + StreamTag: "default", + Stickers: map[string]string{"product": "oss"}, + EndTime: &testTime, + }, + err: errMissingStartTime, + }, + { + name: "missing end time", + input: AnnotationDeleteFilter{ + StreamTag: "default", + Stickers: map[string]string{"product": "oss"}, + StartTime: &testTime, + }, + err: errMissingEndTime, + }, + { + name: "end time before start create request", + input: AnnotationDeleteFilter{ + StreamTag: "default", + Stickers: map[string]string{"product": "oss"}, + EndTime: &testTime, + StartTime: &testTime2, + }, + err: errReversedTimes, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.input.Validate() + if test.err != nil { + require.Equal(t, test.err, err) + return + } + + require.NoError(t, err) + require.Equal(t, test.expected, test.input) + }) + } +} + +func TestAnnotationListFilter(t *testing.T) { + type tst struct { + name string + input AnnotationListFilter + expected AnnotationListFilter + checkValue bool + err *errors.Error + } + + tests := []tst{ + { + name: "minimum valid", + input: AnnotationListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime, + }, + }, + expected: AnnotationListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime, + }, + }, + }, + { + name: "empty valid", + input: AnnotationListFilter{}, + expected: AnnotationListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime, + }, + }, + checkValue: true, + }, + { + name: "invalid due to reversed times", + input: AnnotationListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime2, + }, + }, + err: errReversedTimes, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.input.Validate(nowFunc) + if test.err != nil { + require.Equal(t, test.err, err) + return + } + + require.NoError(t, err) + if test.checkValue { + require.Equal(t, *test.expected.BasicFilter.StartTime, *test.expected.BasicFilter.EndTime) + } else { + require.Equal(t, test.expected, test.input) + } + }) + } +} + +func TestStreamListFilter(t *testing.T) { + type tst struct { + name string + input StreamListFilter + expected StreamListFilter + checkValue bool + err *errors.Error + } + + tests := []tst{ + { + name: "minimum valid", + input: StreamListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime, + }, + }, + expected: StreamListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime, + }, + }, + }, + { + name: "empty valid", + input: StreamListFilter{}, + expected: StreamListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime, + }, + }, + checkValue: true, + }, + { + name: "invalid due to reversed times", + input: StreamListFilter{ + BasicFilter: BasicFilter{ + EndTime: &testTime, + StartTime: &testTime2, + }, + }, + err: errReversedTimes, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.input.Validate(nowFunc) + if test.err != nil { + require.Equal(t, test.err, err) + return + } + + require.NoError(t, err) + if test.checkValue { + require.Equal(t, *test.expected.BasicFilter.StartTime, *test.expected.BasicFilter.EndTime) + } else { + require.Equal(t, test.expected, test.input) + } + }) + } +} + +func TestStreamIsValid(t *testing.T) { + type tst struct { + name string + input Stream + err *errors.Error + } + + tests := []tst{ + { + name: "minimum valid", + input: Stream{ + Name: "default", + }, + }, + { + name: "empty valid", + input: Stream{}, + }, + { + name: "invalid name too long", + input: Stream{ + Name: strings.Repeat("a", 512), + }, + err: errStreamNameTooLong, + }, + { + name: "invalid description too long", + input: Stream{ + Name: "longTom", + Description: strings.Repeat("a", 2048), + }, + err: errStreamDescTooLong, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.err != nil { + require.Equal(t, test.err, test.input.Validate(false)) + } else { + require.NoError(t, test.input.Validate(false)) + } + }) + } +} + +func TestBasicStreamIsValid(t *testing.T) { + type tst struct { + name string + input BasicStream + expected bool + } + + tests := []tst{ + { + name: "minimum valid", + input: BasicStream{ + Names: []string{"default"}, + }, + expected: true, + }, + { + name: "invalid", + input: BasicStream{}, + expected: false, + }, + { + name: "empty name", + input: BasicStream{Names: []string{""}}, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.expected, test.input.IsValid()) + }) + } +} + +func TestMashallReadAnnotations(t *testing.T) { + ra := ReadAnnotations{ + "default": []ReadAnnotation{ + { + ID: *annID, + Summary: "this is one annotation", + Stickers: map[string]string{"env": "testing"}, + StartTime: testTime.Format(time.RFC3339Nano), + EndTime: testTime2.Format(time.RFC3339Nano), + }, + { + ID: *annID, + Summary: "this is another annotation", + Stickers: map[string]string{"env": "testing"}, + StartTime: testTime.Format(time.RFC3339Nano), + EndTime: testTime.Format(time.RFC3339Nano), + }, + }, + "testing": []ReadAnnotation{ + { + ID: *annID, + Summary: "this is yet another annotation", + Stickers: map[string]string{"env": "testing"}, + StartTime: testTime.Format(time.RFC3339Nano), + EndTime: testTime.Format(time.RFC3339Nano), + }, + }, + } + + b, err := json.Marshal(ra) + require.NoError(t, err) + require.Greater(t, len(b), 0) +} + +func TestSetStickerIncludes(t *testing.T) { + type tst struct { + name string + input map[string][]string + expected map[string]string + } + + tests := []tst{ + { + name: "with stickerIncludes", + input: map[string][]string{ + "stickerIncludes[product]": {"oss"}, + "stickerIncludes[author]": {"russ"}, + "streams": {"default", "blogs"}, + }, + expected: map[string]string{ + "product": "oss", + "author": "russ", + }, + }, + { + name: "no sticker includes", + input: map[string][]string{ + "startTime": {"2021-01-13T22%3A17%3A37.953Z"}, + "endTime": {"2021-01-13T22%3A17%3A37.953Z"}, + "streams": {"default", "blogs"}, + }, + expected: map[string]string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := AnnotationListFilter{} + f.SetStickerIncludes(test.input) + require.Equal(t, test.expected, f.StickerIncludes) + }) + } +} + +func TestSetStickers(t *testing.T) { + type tst struct { + name string + input map[string][]string + expected map[string]string + } + + tests := []tst{ + { + name: "with stickers", + input: map[string][]string{ + "stickers[product]": {"oss"}, + "stickers[author]": {"russ"}, + "streams": {"default", "blogs"}, + }, + expected: map[string]string{ + "product": "oss", + "author": "russ", + }, + }, + { + name: "no stickers", + input: map[string][]string{ + "startTime": {"2021-01-13T22%3A17%3A37.953Z"}, + "endTime": {"2021-01-13T22%3A17%3A37.953Z"}, + "streams": {"default", "blogs"}, + }, + expected: map[string]string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f := AnnotationDeleteFilter{} + f.SetStickers(test.input) + require.Equal(t, test.expected, f.Stickers) + }) + } +} diff --git a/mock/annotation_service.go b/mock/annotation_service.go new file mode 100644 index 0000000000..9562591957 --- /dev/null +++ b/mock/annotation_service.go @@ -0,0 +1,198 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: annotation.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + influxdb "github.com/influxdata/influxdb/v2" + platform "github.com/influxdata/influxdb/v2/kit/platform" +) + +// MockAnnotationService is a mock of AnnotationService interface. +type MockAnnotationService struct { + ctrl *gomock.Controller + recorder *MockAnnotationServiceMockRecorder +} + +// MockAnnotationServiceMockRecorder is the mock recorder for MockAnnotationService. +type MockAnnotationServiceMockRecorder struct { + mock *MockAnnotationService +} + +// NewMockAnnotationService creates a new mock instance. +func NewMockAnnotationService(ctrl *gomock.Controller) *MockAnnotationService { + mock := &MockAnnotationService{ctrl: ctrl} + mock.recorder = &MockAnnotationServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAnnotationService) EXPECT() *MockAnnotationServiceMockRecorder { + return m.recorder +} + +// CreateAnnotations mocks base method. +func (m *MockAnnotationService) CreateAnnotations(ctx context.Context, orgID platform.ID, create []influxdb.AnnotationCreate) ([]influxdb.AnnotationEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateAnnotations", ctx, orgID, create) + ret0, _ := ret[0].([]influxdb.AnnotationEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateAnnotations indicates an expected call of CreateAnnotations. +func (mr *MockAnnotationServiceMockRecorder) CreateAnnotations(ctx, orgID, create interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).CreateAnnotations), ctx, orgID, create) +} + +// CreateOrUpdateStream mocks base method. +func (m *MockAnnotationService) CreateOrUpdateStream(ctx context.Context, orgID platform.ID, stream influxdb.Stream) (*influxdb.ReadStream, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateStream", ctx, orgID, stream) + ret0, _ := ret[0].(*influxdb.ReadStream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateStream indicates an expected call of CreateOrUpdateStream. +func (mr *MockAnnotationServiceMockRecorder) CreateOrUpdateStream(ctx, orgID, stream interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateStream", reflect.TypeOf((*MockAnnotationService)(nil).CreateOrUpdateStream), ctx, orgID, stream) +} + +// DeleteAnnotation mocks base method. +func (m *MockAnnotationService) DeleteAnnotation(ctx context.Context, orgID, id platform.ID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, orgID, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAnnotation indicates an expected call of DeleteAnnotation. +func (mr *MockAnnotationServiceMockRecorder) DeleteAnnotation(ctx, orgID, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).DeleteAnnotation), ctx, orgID, id) +} + +// DeleteAnnotations mocks base method. +func (m *MockAnnotationService) DeleteAnnotations(ctx context.Context, orgID platform.ID, delete influxdb.AnnotationDeleteFilter) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAnnotations", ctx, orgID, delete) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAnnotations indicates an expected call of DeleteAnnotations. +func (mr *MockAnnotationServiceMockRecorder) DeleteAnnotations(ctx, orgID, delete interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).DeleteAnnotations), ctx, orgID, delete) +} + +// DeleteStream mocks base method. +func (m *MockAnnotationService) DeleteStream(ctx context.Context, orgID platform.ID, streamName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStream", ctx, orgID, streamName) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStream indicates an expected call of DeleteStream. +func (mr *MockAnnotationServiceMockRecorder) DeleteStream(ctx, orgID, streamName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStream", reflect.TypeOf((*MockAnnotationService)(nil).DeleteStream), ctx, orgID, streamName) +} + +// DeleteStreamByID mocks base method. +func (m *MockAnnotationService) DeleteStreamByID(ctx context.Context, orgID, id platform.ID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStreamByID", ctx, orgID, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStreamByID indicates an expected call of DeleteStreamByID. +func (mr *MockAnnotationServiceMockRecorder) DeleteStreamByID(ctx, orgID, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStreamByID", reflect.TypeOf((*MockAnnotationService)(nil).DeleteStreamByID), ctx, orgID, id) +} + +// GetAnnotation mocks base method. +func (m *MockAnnotationService) GetAnnotation(ctx context.Context, orgID, id platform.ID) (*influxdb.AnnotationEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAnnotation", ctx, orgID, id) + ret0, _ := ret[0].(*influxdb.AnnotationEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAnnotation indicates an expected call of GetAnnotation. +func (mr *MockAnnotationServiceMockRecorder) GetAnnotation(ctx, orgID, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).GetAnnotation), ctx, orgID, id) +} + +// ListAnnotations mocks base method. +func (m *MockAnnotationService) ListAnnotations(ctx context.Context, orgID platform.ID, filter influxdb.AnnotationListFilter) (influxdb.ReadAnnotations, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAnnotations", ctx, orgID, filter) + ret0, _ := ret[0].(influxdb.ReadAnnotations) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAnnotations indicates an expected call of ListAnnotations. +func (mr *MockAnnotationServiceMockRecorder) ListAnnotations(ctx, orgID, filter interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).ListAnnotations), ctx, orgID, filter) +} + +// ListStreams mocks base method. +func (m *MockAnnotationService) ListStreams(ctx context.Context, orgID platform.ID, filter influxdb.StreamListFilter) ([]influxdb.ReadStream, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListStreams", ctx, orgID, filter) + ret0, _ := ret[0].([]influxdb.ReadStream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListStreams indicates an expected call of ListStreams. +func (mr *MockAnnotationServiceMockRecorder) ListStreams(ctx, orgID, filter interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStreams", reflect.TypeOf((*MockAnnotationService)(nil).ListStreams), ctx, orgID, filter) +} + +// UpdateAnnotation mocks base method. +func (m *MockAnnotationService) UpdateAnnotation(ctx context.Context, orgID, id platform.ID, update influxdb.AnnotationCreate) (*influxdb.AnnotationEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAnnotation", ctx, orgID, id, update) + ret0, _ := ret[0].(*influxdb.AnnotationEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateAnnotation indicates an expected call of UpdateAnnotation. +func (mr *MockAnnotationServiceMockRecorder) UpdateAnnotation(ctx, orgID, id, update interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).UpdateAnnotation), ctx, orgID, id, update) +} + +// UpdateStream mocks base method. +func (m *MockAnnotationService) UpdateStream(ctx context.Context, orgID, id platform.ID, stream influxdb.Stream) (*influxdb.ReadStream, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateStream", ctx, orgID, id, stream) + ret0, _ := ret[0].(*influxdb.ReadStream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateStream indicates an expected call of UpdateStream. +func (mr *MockAnnotationServiceMockRecorder) UpdateStream(ctx, orgID, id, stream interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStream", reflect.TypeOf((*MockAnnotationService)(nil).UpdateStream), ctx, orgID, id, stream) +}