1// Copyright 2019 Prometheus Team
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package test
15
16import (
17	"encoding/json"
18	"fmt"
19	"net"
20	"net/http"
21	"reflect"
22	"time"
23
24	"github.com/go-openapi/strfmt"
25
26	"github.com/prometheus/alertmanager/api/v2/models"
27	"github.com/prometheus/alertmanager/notify/webhook"
28)
29
30// At is a convenience method to allow for declarative syntax of Acceptance
31// test definitions.
32func At(ts float64) float64 {
33	return ts
34}
35
36type Interval struct {
37	start, end float64
38}
39
40func (iv Interval) String() string {
41	return fmt.Sprintf("[%v,%v]", iv.start, iv.end)
42}
43
44func (iv Interval) contains(f float64) bool {
45	return f >= iv.start && f <= iv.end
46}
47
48// Between is a convenience constructor for an interval for declarative syntax
49// of Acceptance test definitions.
50func Between(start, end float64) Interval {
51	return Interval{start: start, end: end}
52}
53
54// TestSilence models a model.Silence with relative times.
55type TestSilence struct {
56	id               string
57	createdBy        string
58	match            []string
59	matchRE          []string
60	startsAt, endsAt float64
61	comment          string
62}
63
64// Silence creates a new TestSilence active for the relative interval given
65// by start and end.
66func Silence(start, end float64) *TestSilence {
67	return &TestSilence{
68		startsAt: start,
69		endsAt:   end,
70	}
71}
72
73// Match adds a new plain matcher to the silence.
74func (s *TestSilence) Match(v ...string) *TestSilence {
75	s.match = append(s.match, v...)
76	return s
77}
78
79// MatchRE adds a new regex matcher to the silence
80func (s *TestSilence) MatchRE(v ...string) *TestSilence {
81	if len(v)%2 == 1 {
82		panic("bad key/values")
83	}
84	s.matchRE = append(s.matchRE, v...)
85	return s
86}
87
88// Comment sets the comment to the silence.
89func (s *TestSilence) Comment(c string) *TestSilence {
90	s.comment = c
91	return s
92}
93
94// SetID sets the silence ID.
95func (s *TestSilence) SetID(ID string) {
96	s.id = ID
97}
98
99// ID gets the silence ID.
100func (s *TestSilence) ID() string {
101	return s.id
102}
103
104// TestAlert models a model.Alert with relative times.
105type TestAlert struct {
106	labels           models.LabelSet
107	annotations      models.LabelSet
108	startsAt, endsAt float64
109	summary          string
110}
111
112// Alert creates a new alert declaration with the given key/value pairs
113// as identifying labels.
114func Alert(keyval ...interface{}) *TestAlert {
115	if len(keyval)%2 == 1 {
116		panic("bad key/values")
117	}
118	a := &TestAlert{
119		labels:      models.LabelSet{},
120		annotations: models.LabelSet{},
121	}
122
123	for i := 0; i < len(keyval); i += 2 {
124		ln := keyval[i].(string)
125		lv := keyval[i+1].(string)
126
127		a.labels[ln] = lv
128	}
129
130	return a
131}
132
133// nativeAlert converts the declared test alert into a full alert based
134// on the given parameters.
135func (a *TestAlert) nativeAlert(opts *AcceptanceOpts) *models.GettableAlert {
136	na := &models.GettableAlert{
137		Alert: models.Alert{
138			Labels: a.labels,
139		},
140		Annotations: a.annotations,
141		StartsAt:    &strfmt.DateTime{},
142		EndsAt:      &strfmt.DateTime{},
143	}
144
145	if a.startsAt > 0 {
146		start := strfmt.DateTime(opts.expandTime(a.startsAt))
147		na.StartsAt = &start
148	}
149	if a.endsAt > 0 {
150		end := strfmt.DateTime(opts.expandTime(a.endsAt))
151		na.EndsAt = &end
152	}
153
154	return na
155}
156
157// Annotate the alert with the given key/value pairs.
158func (a *TestAlert) Annotate(keyval ...interface{}) *TestAlert {
159	if len(keyval)%2 == 1 {
160		panic("bad key/values")
161	}
162
163	for i := 0; i < len(keyval); i += 2 {
164		ln := keyval[i].(string)
165		lv := keyval[i+1].(string)
166
167		a.annotations[ln] = lv
168	}
169
170	return a
171}
172
173// Active declares the relative activity time for this alert. It
174// must be a single starting value or two values where the second value
175// declares the resolved time.
176func (a *TestAlert) Active(tss ...float64) *TestAlert {
177
178	if len(tss) > 2 || len(tss) == 0 {
179		panic("only one or two timestamps allowed")
180	}
181	if len(tss) == 2 {
182		a.endsAt = tss[1]
183	}
184	a.startsAt = tss[0]
185
186	return a
187}
188
189func equalAlerts(a, b *models.GettableAlert, opts *AcceptanceOpts) bool {
190	if !reflect.DeepEqual(a.Labels, b.Labels) {
191		return false
192	}
193	if !reflect.DeepEqual(a.Annotations, b.Annotations) {
194		return false
195	}
196
197	if !equalTime(time.Time(*a.StartsAt), time.Time(*b.StartsAt), opts) {
198		return false
199	}
200	if (a.EndsAt == nil) != (b.EndsAt == nil) {
201		return false
202	}
203	if !(a.EndsAt == nil) && !(b.EndsAt == nil) && !equalTime(time.Time(*a.EndsAt), time.Time(*b.EndsAt), opts) {
204		return false
205	}
206	return true
207}
208
209func equalTime(a, b time.Time, opts *AcceptanceOpts) bool {
210	if a.IsZero() != b.IsZero() {
211		return false
212	}
213
214	diff := a.Sub(b)
215	if diff < 0 {
216		diff = -diff
217	}
218	return diff <= opts.Tolerance
219}
220
221type MockWebhook struct {
222	opts      *AcceptanceOpts
223	collector *Collector
224	listener  net.Listener
225
226	// Func is called early on when retrieving a notification by an
227	// Alertmanager. If Func returns true, the given notification is dropped.
228	// See sample usage in `send_test.go/TestRetry()`.
229	Func func(timestamp float64) bool
230}
231
232func NewWebhook(c *Collector) *MockWebhook {
233	l, err := net.Listen("tcp4", "localhost:0")
234	if err != nil {
235		// TODO(fabxc): if shutdown of mock destinations ever becomes a concern
236		// we want to shut them down after test completion. Then we might want to
237		// log the error properly, too.
238		panic(err)
239	}
240	wh := &MockWebhook{
241		listener:  l,
242		collector: c,
243		opts:      c.opts,
244	}
245	go func() {
246		if err := http.Serve(l, wh); err != nil {
247			panic(err)
248		}
249	}()
250
251	return wh
252}
253
254func (ws *MockWebhook) ServeHTTP(w http.ResponseWriter, req *http.Request) {
255	// Inject Func if it exists.
256	if ws.Func != nil {
257		if ws.Func(ws.opts.relativeTime(time.Now())) {
258			return
259		}
260	}
261
262	dec := json.NewDecoder(req.Body)
263	defer req.Body.Close()
264
265	var v webhook.Message
266	if err := dec.Decode(&v); err != nil {
267		panic(err)
268	}
269
270	// Transform the webhook message alerts back into model.Alerts.
271	var alerts models.GettableAlerts
272	for _, a := range v.Alerts {
273		var (
274			labels      = models.LabelSet{}
275			annotations = models.LabelSet{}
276		)
277		for k, v := range a.Labels {
278			labels[k] = v
279		}
280		for k, v := range a.Annotations {
281			annotations[k] = v
282		}
283
284		start := strfmt.DateTime(a.StartsAt)
285		end := strfmt.DateTime(a.EndsAt)
286
287		alerts = append(alerts, &models.GettableAlert{
288			Alert: models.Alert{
289				Labels:       labels,
290				GeneratorURL: strfmt.URI(a.GeneratorURL),
291			},
292			Annotations: annotations,
293			StartsAt:    &start,
294			EndsAt:      &end,
295		})
296	}
297
298	ws.collector.add(alerts...)
299}
300
301func (ws *MockWebhook) Address() string {
302	return ws.listener.Addr().String()
303}
304