1package sentry
2
3import (
4	"context"
5	"fmt"
6	"sync"
7	"testing"
8
9	"github.com/google/go-cmp/cmp"
10	"github.com/google/go-cmp/cmp/cmpopts"
11)
12
13func setupHubTest() (*Hub, *Client, *Scope) {
14	client, _ := NewClient(ClientOptions{Dsn: "http://whatever@really.com/1337"})
15	scope := NewScope()
16	hub := NewHub(client, scope)
17	return hub, client, scope
18}
19
20func TestNewHubPushesLayerOnTopOfStack(t *testing.T) {
21	hub, _, _ := setupHubTest()
22	assertEqual(t, len(*hub.stack), 1)
23}
24
25func TestNewHubLayerStoresClientAndScope(t *testing.T) {
26	hub, client, scope := setupHubTest()
27	assertEqual(t, &layer{client: client, scope: scope}, (*hub.stack)[0])
28}
29
30func TestCloneHubInheritsClientAndScope(t *testing.T) {
31	hub, client, scope := setupHubTest()
32	clone := hub.Clone()
33
34	if hub == clone {
35		t.Error("Cloned hub should be a new instance")
36	}
37
38	if clone.Client() != client {
39		t.Error("Client should be inherited")
40	}
41
42	if clone.Scope() == scope {
43		t.Error("Scope should be cloned, not reused")
44	}
45
46	assertEqual(t, clone.Scope(), scope)
47}
48
49func TestPushScopeAddsScopeOnTopOfStack(t *testing.T) {
50	hub, _, _ := setupHubTest()
51	hub.PushScope()
52	assertEqual(t, len(*hub.stack), 2)
53}
54
55func TestPushScopeInheritsScopeData(t *testing.T) {
56	hub, _, scope := setupHubTest()
57	scope.SetExtra("foo", "bar")
58	hub.PushScope()
59	scope.SetExtra("baz", "qux")
60
61	if (*hub.stack)[0].scope == (*hub.stack)[1].scope {
62		t.Error("Scope shouldnt point to the same struct")
63	}
64	assertEqual(t, map[string]interface{}{"foo": "bar", "baz": "qux"}, (*hub.stack)[0].scope.extra)
65	assertEqual(t, map[string]interface{}{"foo": "bar"}, (*hub.stack)[1].scope.extra)
66}
67
68func TestPushScopeInheritsClient(t *testing.T) {
69	hub, _, _ := setupHubTest()
70	hub.PushScope()
71
72	if (*hub.stack)[0].client != (*hub.stack)[1].client {
73		t.Error("Client should be inherited")
74	}
75}
76
77func TestPopScopeRemovesLayerFromTheStack(t *testing.T) {
78	hub, _, _ := setupHubTest()
79	hub.PushScope()
80	hub.PushScope()
81	hub.PopScope()
82
83	assertEqual(t, len(*hub.stack), 2)
84}
85
86func TestPopScopeCannotLeaveStackEmpty(t *testing.T) {
87	hub, _, _ := setupHubTest()
88	assertEqual(t, len(*hub.stack), 1)
89	hub.PopScope()
90	assertEqual(t, len(*hub.stack), 1)
91}
92
93func TestBindClient(t *testing.T) {
94	hub, client, _ := setupHubTest()
95	hub.PushScope()
96	newClient, _ := NewClient(ClientOptions{Dsn: "http://whatever@really.com/1337"})
97	hub.BindClient(newClient)
98
99	if (*hub.stack)[0].client == (*hub.stack)[1].client {
100		t.Error("Two stack layers should have different clients bound")
101	}
102	if (*hub.stack)[0].client != client {
103		t.Error("Stack's parent layer should have old client bound")
104	}
105	if (*hub.stack)[1].client != newClient {
106		t.Error("Stack's top layer should have new client bound")
107	}
108}
109
110func TestWithScopeCreatesIsolatedScope(t *testing.T) {
111	hub, _, _ := setupHubTest()
112
113	hub.WithScope(func(scope *Scope) {
114		assertEqual(t, len(*hub.stack), 2)
115	})
116
117	assertEqual(t, len(*hub.stack), 1)
118}
119
120func TestWithScopeBindClient(t *testing.T) {
121	hub, client, _ := setupHubTest()
122
123	hub.WithScope(func(scope *Scope) {
124		newClient, _ := NewClient(ClientOptions{Dsn: "http://whatever@really.com/1337"})
125		hub.BindClient(newClient)
126		if hub.stackTop().client != newClient {
127			t.Error("should use newly bound client")
128		}
129	})
130
131	if hub.stackTop().client != client {
132		t.Error("should use old client")
133	}
134}
135
136func TestWithScopeDirectChanges(t *testing.T) {
137	hub, _, _ := setupHubTest()
138	hub.Scope().SetExtra("extra", "foo")
139
140	hub.WithScope(func(scope *Scope) {
141		scope.SetExtra("extra", "bar")
142		assertEqual(t, map[string]interface{}{"extra": "bar"}, hub.stackTop().scope.extra)
143	})
144
145	assertEqual(t, map[string]interface{}{"extra": "foo"}, hub.stackTop().scope.extra)
146}
147
148func TestWithScopeChangesThroughConfigureScope(t *testing.T) {
149	hub, _, _ := setupHubTest()
150	hub.Scope().SetExtra("extra", "foo")
151
152	hub.WithScope(func(scope *Scope) {
153		hub.ConfigureScope(func(scope *Scope) {
154			scope.SetExtra("extra", "bar")
155		})
156		assertEqual(t, map[string]interface{}{"extra": "bar"}, hub.stackTop().scope.extra)
157	})
158
159	assertEqual(t, map[string]interface{}{"extra": "foo"}, hub.stackTop().scope.extra)
160}
161
162func TestConfigureScope(t *testing.T) {
163	hub, _, _ := setupHubTest()
164	hub.Scope().SetExtra("extra", "foo")
165
166	hub.ConfigureScope(func(scope *Scope) {
167		scope.SetExtra("extra", "bar")
168		assertEqual(t, map[string]interface{}{"extra": "bar"}, hub.stackTop().scope.extra)
169	})
170
171	assertEqual(t, map[string]interface{}{"extra": "bar"}, hub.stackTop().scope.extra)
172}
173
174func TestLastEventID(t *testing.T) {
175	uuid := EventID(uuid())
176	hub := &Hub{lastEventID: uuid}
177	assertEqual(t, uuid, hub.LastEventID())
178}
179
180func TestLastEventIDUpdatesAfterCaptures(t *testing.T) {
181	hub, _, _ := setupHubTest()
182
183	messageID := hub.CaptureMessage("wat")
184	assertEqual(t, *messageID, hub.LastEventID())
185
186	errorID := hub.CaptureException(fmt.Errorf("wat"))
187	assertEqual(t, *errorID, hub.LastEventID())
188
189	eventID := hub.CaptureEvent(&Event{Message: "wat"})
190	assertEqual(t, *eventID, hub.LastEventID())
191}
192
193func TestAddBreadcrumbRespectMaxBreadcrumbsOption(t *testing.T) {
194	hub, client, scope := setupHubTest()
195	client.options.MaxBreadcrumbs = 2
196
197	breadcrumb := &Breadcrumb{Message: "Breadcrumb"}
198
199	hub.AddBreadcrumb(breadcrumb, nil)
200	hub.AddBreadcrumb(breadcrumb, nil)
201	hub.AddBreadcrumb(breadcrumb, nil)
202
203	assertEqual(t, len(scope.breadcrumbs), 2)
204}
205
206func TestAddBreadcrumbSkipAllBreadcrumbsIfMaxBreadcrumbsIsLessThanZero(t *testing.T) {
207	hub, client, scope := setupHubTest()
208	client.options.MaxBreadcrumbs = -1
209
210	breadcrumb := &Breadcrumb{Message: "Breadcrumb"}
211
212	hub.AddBreadcrumb(breadcrumb, nil)
213	hub.AddBreadcrumb(breadcrumb, nil)
214	hub.AddBreadcrumb(breadcrumb, nil)
215
216	assertEqual(t, len(scope.breadcrumbs), 0)
217}
218
219func TestAddBreadcrumbShouldNeverExceedMaxBreadcrumbsConst(t *testing.T) {
220	hub, client, scope := setupHubTest()
221	client.options.MaxBreadcrumbs = 1000
222
223	breadcrumb := &Breadcrumb{Message: "Breadcrumb"}
224
225	for i := 0; i < 111; i++ {
226		hub.AddBreadcrumb(breadcrumb, nil)
227	}
228
229	assertEqual(t, len(scope.breadcrumbs), 100)
230}
231
232func TestAddBreadcrumbShouldWorkWithoutClient(t *testing.T) {
233	scope := NewScope()
234	hub := NewHub(nil, scope)
235
236	breadcrumb := &Breadcrumb{Message: "Breadcrumb"}
237	for i := 0; i < 111; i++ {
238		hub.AddBreadcrumb(breadcrumb, nil)
239	}
240
241	assertEqual(t, len(scope.breadcrumbs), 100)
242}
243
244func TestAddBreadcrumbCallsBeforeBreadcrumbCallback(t *testing.T) {
245	hub, client, scope := setupHubTest()
246	client.options.BeforeBreadcrumb = func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb {
247		breadcrumb.Message += "_wat"
248		return breadcrumb
249	}
250
251	hub.AddBreadcrumb(&Breadcrumb{Message: "Breadcrumb"}, nil)
252
253	assertEqual(t, len(scope.breadcrumbs), 1)
254	assertEqual(t, "Breadcrumb_wat", scope.breadcrumbs[0].Message)
255}
256
257func TestBeforeBreadcrumbCallbackCanDropABreadcrumb(t *testing.T) {
258	hub, client, scope := setupHubTest()
259	client.options.BeforeBreadcrumb = func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb {
260		return nil
261	}
262
263	hub.AddBreadcrumb(&Breadcrumb{Message: "Breadcrumb"}, nil)
264	hub.AddBreadcrumb(&Breadcrumb{Message: "Breadcrumb"}, nil)
265
266	assertEqual(t, len(scope.breadcrumbs), 0)
267}
268
269func TestBeforeBreadcrumbGetAccessToEventHint(t *testing.T) {
270	hub, client, scope := setupHubTest()
271	client.options.BeforeBreadcrumb = func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb {
272		if val, ok := (*hint)["foo"]; ok {
273			if val, ok := val.(string); ok {
274				breadcrumb.Message += val
275			}
276		}
277
278		return breadcrumb
279	}
280
281	hub.AddBreadcrumb(&Breadcrumb{Message: "Breadcrumb"}, &BreadcrumbHint{"foo": "_oh"})
282
283	assertEqual(t, len(scope.breadcrumbs), 1)
284	assertEqual(t, "Breadcrumb_oh", scope.breadcrumbs[0].Message)
285}
286
287func TestHasHubOnContextReturnsTrueIfHubIsThere(t *testing.T) {
288	hub, _, _ := setupHubTest()
289	ctx := context.Background()
290	ctx = SetHubOnContext(ctx, hub)
291	assertEqual(t, true, HasHubOnContext(ctx))
292}
293
294func TestHasHubOnContextReturnsFalseIfHubIsNotThere(t *testing.T) {
295	ctx := context.Background()
296	assertEqual(t, false, HasHubOnContext(ctx))
297}
298
299func TestGetHubFromContext(t *testing.T) {
300	hub, _, _ := setupHubTest()
301	ctx := context.Background()
302	ctx = SetHubOnContext(ctx, hub)
303	hubFromContext := GetHubFromContext(ctx)
304	assertEqual(t, hub, hubFromContext)
305}
306
307func TestGetHubFromContextReturnsNilIfHubIsNotThere(t *testing.T) {
308	ctx := context.Background()
309	hub := GetHubFromContext(ctx)
310	if hub != nil {
311		t.Error("hub shouldnt be available on empty context")
312	}
313}
314
315func TestSetHubOnContextReturnsNewContext(t *testing.T) {
316	hub, _, _ := setupHubTest()
317	ctx := context.Background()
318	ctxWithHub := SetHubOnContext(ctx, hub)
319	if ctx == ctxWithHub {
320		t.Error("contexts should be different")
321	}
322}
323
324func TestConcurrentHubClone(t *testing.T) {
325	const goroutineCount = 3
326
327	hub, client, _ := setupHubTest()
328	transport := &TransportMock{}
329	client.Transport = transport
330
331	var wg sync.WaitGroup
332	wg.Add(goroutineCount)
333	for i := 1; i <= goroutineCount; i++ {
334		// Mutate hub in the main goroutine.
335		hub.PushScope()
336		hub.PopScope()
337		hub.BindClient(client)
338		// Clone scope in a new Goroutine as documented in
339		// https://docs.sentry.io/platforms/go/goroutines/.
340		go func(i int) {
341			defer wg.Done()
342			localHub := hub.Clone()
343			localHub.ConfigureScope(func(scope *Scope) {
344				scope.SetTag("secretTag", fmt.Sprintf("go#%d", i))
345			})
346			localHub.CaptureMessage(fmt.Sprintf("Hello from goroutine! #%d", i))
347		}(i)
348	}
349	wg.Wait()
350
351	type TestEvent struct {
352		Message string
353		Tags    map[string]string
354	}
355
356	want := []TestEvent{
357		{
358			Message: "Hello from goroutine! #1",
359			Tags:    map[string]string{"secretTag": "go#1"},
360		},
361		{
362			Message: "Hello from goroutine! #2",
363			Tags:    map[string]string{"secretTag": "go#2"},
364		},
365		{
366			Message: "Hello from goroutine! #3",
367			Tags:    map[string]string{"secretTag": "go#3"},
368		},
369	}
370
371	var got []TestEvent
372	for _, event := range transport.Events() {
373		got = append(got, TestEvent{
374			Message: event.Message,
375			Tags:    event.Tags,
376		})
377	}
378
379	if diff := cmp.Diff(want, got, cmpopts.SortSlices(func(x, y TestEvent) bool {
380		return x.Message < y.Message
381	})); diff != "" {
382		t.Errorf("Events mismatch (-want +got):\n%s", diff)
383	}
384}
385