1package api
2
3import (
4	"context"
5	"strings"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/assert"
10)
11
12func TestAPI_SessionCreateDestroy(t *testing.T) {
13	t.Parallel()
14	c, s := makeClient(t)
15	defer s.Stop()
16
17	s.WaitForSerfCheck(t)
18
19	session := c.Session()
20
21	id, meta, err := session.Create(nil, nil)
22	if err != nil {
23		t.Fatalf("err: %v", err)
24	}
25
26	if meta.RequestTime == 0 {
27		t.Fatalf("bad: %v", meta)
28	}
29
30	if id == "" {
31		t.Fatalf("invalid: %v", id)
32	}
33
34	meta, err = session.Destroy(id, nil)
35	if err != nil {
36		t.Fatalf("err: %v", err)
37	}
38
39	if meta.RequestTime == 0 {
40		t.Fatalf("bad: %v", meta)
41	}
42}
43
44func TestAPI_SessionCreateRenewDestroy(t *testing.T) {
45	t.Parallel()
46	c, s := makeClient(t)
47	defer s.Stop()
48
49	s.WaitForSerfCheck(t)
50
51	session := c.Session()
52
53	se := &SessionEntry{
54		TTL: "10s",
55	}
56
57	id, meta, err := session.Create(se, nil)
58	if err != nil {
59		t.Fatalf("err: %v", err)
60	}
61	defer session.Destroy(id, nil)
62
63	if meta.RequestTime == 0 {
64		t.Fatalf("bad: %v", meta)
65	}
66
67	if id == "" {
68		t.Fatalf("invalid: %v", id)
69	}
70
71	if meta.RequestTime == 0 {
72		t.Fatalf("bad: %v", meta)
73	}
74
75	renew, meta, err := session.Renew(id, nil)
76
77	if err != nil {
78		t.Fatalf("err: %v", err)
79	}
80	if meta.RequestTime == 0 {
81		t.Fatalf("bad: %v", meta)
82	}
83
84	if renew == nil {
85		t.Fatalf("should get session")
86	}
87
88	if renew.ID != id {
89		t.Fatalf("should have matching id")
90	}
91
92	if renew.TTL != "10s" {
93		t.Fatalf("should get session with TTL")
94	}
95}
96
97func TestAPI_SessionCreateRenewDestroyRenew(t *testing.T) {
98	t.Parallel()
99	c, s := makeClient(t)
100	defer s.Stop()
101
102	s.WaitForSerfCheck(t)
103
104	session := c.Session()
105
106	entry := &SessionEntry{
107		Behavior: SessionBehaviorDelete,
108		TTL:      "500s", // disable ttl
109	}
110
111	id, meta, err := session.Create(entry, nil)
112	if err != nil {
113		t.Fatalf("err: %v", err)
114	}
115
116	if meta.RequestTime == 0 {
117		t.Fatalf("bad: %v", meta)
118	}
119
120	if id == "" {
121		t.Fatalf("invalid: %v", id)
122	}
123
124	// Extend right after create. Everything should be fine.
125	entry, _, err = session.Renew(id, nil)
126	if err != nil {
127		t.Fatalf("err: %v", err)
128	}
129	if entry == nil {
130		t.Fatal("session unexpectedly vanished")
131	}
132
133	// Simulate TTL loss by manually destroying the session.
134	meta, err = session.Destroy(id, nil)
135	if err != nil {
136		t.Fatalf("err: %v", err)
137	}
138
139	if meta.RequestTime == 0 {
140		t.Fatalf("bad: %v", meta)
141	}
142
143	// Extend right after delete. The 404 should proxy as a nil.
144	entry, _, err = session.Renew(id, nil)
145	if err != nil {
146		t.Fatalf("err: %v", err)
147	}
148	if entry != nil {
149		t.Fatal("session still exists")
150	}
151}
152
153func TestAPI_SessionCreateDestroyRenewPeriodic(t *testing.T) {
154	t.Parallel()
155	c, s := makeClient(t)
156	defer s.Stop()
157
158	s.WaitForSerfCheck(t)
159
160	session := c.Session()
161
162	entry := &SessionEntry{
163		Behavior: SessionBehaviorDelete,
164		TTL:      "500s", // disable ttl
165	}
166
167	id, meta, err := session.Create(entry, nil)
168	if err != nil {
169		t.Fatalf("err: %v", err)
170	}
171
172	if meta.RequestTime == 0 {
173		t.Fatalf("bad: %v", meta)
174	}
175
176	if id == "" {
177		t.Fatalf("invalid: %v", id)
178	}
179
180	// This only tests Create/Destroy/RenewPeriodic to avoid the more
181	// difficult case of testing all of the timing code.
182
183	// Simulate TTL loss by manually destroying the session.
184	meta, err = session.Destroy(id, nil)
185	if err != nil {
186		t.Fatalf("err: %v", err)
187	}
188
189	if meta.RequestTime == 0 {
190		t.Fatalf("bad: %v", meta)
191	}
192
193	// Extend right after delete. The 404 should terminate the loop quickly and return ErrSessionExpired.
194	errCh := make(chan error, 1)
195	doneCh := make(chan struct{})
196	go func() { errCh <- session.RenewPeriodic("1s", id, nil, doneCh) }()
197	defer close(doneCh)
198
199	select {
200	case <-time.After(1 * time.Second):
201		t.Fatal("timedout: missing session did not terminate renewal loop")
202	case err = <-errCh:
203		if err != ErrSessionExpired {
204			t.Fatalf("err: %v", err)
205		}
206	}
207}
208
209func TestAPI_SessionRenewPeriodic_Cancel(t *testing.T) {
210	t.Parallel()
211	c, s := makeClient(t)
212	defer s.Stop()
213
214	s.WaitForSerfCheck(t)
215
216	session := c.Session()
217	entry := &SessionEntry{
218		Behavior: SessionBehaviorDelete,
219		TTL:      "500s", // disable ttl
220	}
221
222	t.Run("done channel", func(t *testing.T) {
223		id, _, err := session.Create(entry, nil)
224		if err != nil {
225			t.Fatalf("err: %v", err)
226		}
227
228		errCh := make(chan error, 1)
229		doneCh := make(chan struct{})
230		go func() { errCh <- session.RenewPeriodic("1s", id, nil, doneCh) }()
231
232		close(doneCh)
233
234		select {
235		case <-time.After(1 * time.Second):
236			t.Fatal("renewal loop didn't terminate")
237		case err = <-errCh:
238			if err != nil {
239				t.Fatalf("err: %v", err)
240			}
241		}
242
243		sess, _, err := session.Info(id, nil)
244		if err != nil {
245			t.Fatalf("err: %v", err)
246		}
247		if sess != nil {
248			t.Fatalf("session was not expired")
249		}
250	})
251
252	t.Run("context", func(t *testing.T) {
253		id, _, err := session.Create(entry, nil)
254		if err != nil {
255			t.Fatalf("err: %v", err)
256		}
257
258		ctx, cancel := context.WithCancel(context.Background())
259		wo := new(WriteOptions).WithContext(ctx)
260
261		errCh := make(chan error, 1)
262		go func() { errCh <- session.RenewPeriodic("1s", id, wo, nil) }()
263
264		cancel()
265
266		select {
267		case <-time.After(1 * time.Second):
268			t.Fatal("renewal loop didn't terminate")
269		case err = <-errCh:
270			if err == nil || !strings.Contains(err.Error(), "context canceled") {
271				t.Fatalf("err: %v", err)
272			}
273		}
274
275		// See comment in session.go for why the session isn't removed
276		// in this case.
277		sess, _, err := session.Info(id, nil)
278		if err != nil {
279			t.Fatalf("err: %v", err)
280		}
281		if sess == nil {
282			t.Fatalf("session should not be expired")
283		}
284	})
285}
286
287func TestAPI_SessionInfo(t *testing.T) {
288	t.Parallel()
289	c, s := makeClient(t)
290	defer s.Stop()
291
292	s.WaitForSerfCheck(t)
293
294	session := c.Session()
295
296	id, _, err := session.Create(nil, nil)
297	if err != nil {
298		t.Fatalf("err: %v", err)
299	}
300	defer session.Destroy(id, nil)
301
302	info, qm, err := session.Info(id, nil)
303	if err != nil {
304		t.Fatalf("err: %v", err)
305	}
306	if qm.LastIndex == 0 {
307		t.Fatalf("bad: %v", qm)
308	}
309	if !qm.KnownLeader {
310		t.Fatalf("bad: %v", qm)
311	}
312
313	if info.CreateIndex == 0 {
314		t.Fatalf("bad: %v", info)
315	}
316	info.CreateIndex = 0
317
318	want := &SessionEntry{
319		ID:         id,
320		Node:       s.Config.NodeName,
321		NodeChecks: []string{"serfHealth"},
322		LockDelay:  15 * time.Second,
323		Behavior:   SessionBehaviorRelease,
324	}
325	if info.ID != want.ID {
326		t.Fatalf("bad ID: %s", info.ID)
327	}
328	if info.Node != want.Node {
329		t.Fatalf("bad Node: %s", info.Node)
330	}
331	if info.LockDelay != want.LockDelay {
332		t.Fatalf("bad LockDelay: %d", info.LockDelay)
333	}
334	if info.Behavior != want.Behavior {
335		t.Fatalf("bad Behavior: %s", info.Behavior)
336	}
337	if len(info.NodeChecks) != len(want.NodeChecks) {
338		t.Fatalf("expected %d nodechecks, got %d", len(want.NodeChecks), len(info.NodeChecks))
339	}
340	if info.NodeChecks[0] != want.NodeChecks[0] {
341		t.Fatalf("expected nodecheck %s, got %s", want.NodeChecks, info.NodeChecks)
342	}
343}
344
345func TestAPI_SessionInfo_NoChecks(t *testing.T) {
346	t.Parallel()
347	c, s := makeClient(t)
348	defer s.Stop()
349
350	s.WaitForSerfCheck(t)
351
352	session := c.Session()
353
354	id, _, err := session.CreateNoChecks(nil, nil)
355	if err != nil {
356		t.Fatalf("err: %v", err)
357	}
358	defer session.Destroy(id, nil)
359
360	info, qm, err := session.Info(id, nil)
361	if err != nil {
362		t.Fatalf("err: %v", err)
363	}
364
365	if qm.LastIndex == 0 {
366		t.Fatalf("bad: %v", qm)
367	}
368	if !qm.KnownLeader {
369		t.Fatalf("bad: %v", qm)
370	}
371
372	if info.CreateIndex == 0 {
373		t.Fatalf("bad: %v", info)
374	}
375	info.CreateIndex = 0
376
377	want := &SessionEntry{
378		ID:         id,
379		Node:       s.Config.NodeName,
380		NodeChecks: []string{},
381		LockDelay:  15 * time.Second,
382		Behavior:   SessionBehaviorRelease,
383	}
384	if info.ID != want.ID {
385		t.Fatalf("bad ID: %s", info.ID)
386	}
387	if info.Node != want.Node {
388		t.Fatalf("bad Node: %s", info.Node)
389	}
390	if info.LockDelay != want.LockDelay {
391		t.Fatalf("bad LockDelay: %d", info.LockDelay)
392	}
393	if info.Behavior != want.Behavior {
394		t.Fatalf("bad Behavior: %s", info.Behavior)
395	}
396	assert.Equal(t, want.Checks, info.Checks)
397	assert.Equal(t, want.NodeChecks, info.NodeChecks)
398}
399
400func TestAPI_SessionNode(t *testing.T) {
401	t.Parallel()
402	c, s := makeClient(t)
403	defer s.Stop()
404
405	s.WaitForSerfCheck(t)
406
407	session := c.Session()
408
409	id, _, err := session.Create(nil, nil)
410	if err != nil {
411		t.Fatalf("err: %v", err)
412	}
413	defer session.Destroy(id, nil)
414
415	info, _, err := session.Info(id, nil)
416	if err != nil {
417		t.Fatalf("err: %v", err)
418	}
419
420	sessions, qm, err := session.Node(info.Node, nil)
421	if err != nil {
422		t.Fatalf("err: %v", err)
423	}
424
425	if len(sessions) != 1 {
426		t.Fatalf("bad: %v", sessions)
427	}
428
429	if qm.LastIndex == 0 {
430		t.Fatalf("bad: %v", qm)
431	}
432	if !qm.KnownLeader {
433		t.Fatalf("bad: %v", qm)
434	}
435}
436
437func TestAPI_SessionList(t *testing.T) {
438	t.Parallel()
439	c, s := makeClient(t)
440	defer s.Stop()
441
442	s.WaitForSerfCheck(t)
443
444	session := c.Session()
445
446	id, _, err := session.Create(nil, nil)
447	if err != nil {
448		t.Fatalf("err: %v", err)
449	}
450	defer session.Destroy(id, nil)
451
452	sessions, qm, err := session.List(nil)
453	if err != nil {
454		t.Fatalf("err: %v", err)
455	}
456
457	if len(sessions) != 1 {
458		t.Fatalf("bad: %v", sessions)
459	}
460
461	if qm.LastIndex == 0 {
462		t.Fatalf("bad: %v", qm)
463	}
464	if !qm.KnownLeader {
465		t.Fatalf("bad: %v", qm)
466	}
467}
468
469func TestAPI_SessionNodeChecks(t *testing.T) {
470	t.Parallel()
471	c, s := makeClient(t)
472	defer s.Stop()
473
474	s.WaitForSerfCheck(t)
475
476	// Node check that doesn't exist should yield error on creation
477	se := SessionEntry{
478		NodeChecks: []string{"dne"},
479	}
480	session := c.Session()
481
482	_, _, err := session.Create(&se, nil)
483	if err == nil {
484		t.Fatalf("should have failed")
485	}
486
487	// Empty node check should lead to serf check
488	se.NodeChecks = []string{}
489	id, _, err := session.Create(&se, nil)
490	if err != nil {
491		t.Fatalf("err: %v", err)
492	}
493	defer session.Destroy(id, nil)
494
495	info, qm, err := session.Info(id, nil)
496	if err != nil {
497		t.Fatalf("err: %v", err)
498	}
499	if qm.LastIndex == 0 {
500		t.Fatalf("bad: %v", qm)
501	}
502	if !qm.KnownLeader {
503		t.Fatalf("bad: %v", qm)
504	}
505	if info.CreateIndex == 0 {
506		t.Fatalf("bad: %v", info)
507	}
508	info.CreateIndex = 0
509
510	want := &SessionEntry{
511		ID:         id,
512		Node:       s.Config.NodeName,
513		NodeChecks: []string{"serfHealth"},
514		LockDelay:  15 * time.Second,
515		Behavior:   SessionBehaviorRelease,
516	}
517	want.Namespace = info.Namespace
518	assert.Equal(t, want, info)
519
520	// Register a new node with a non-serf check
521	cr := CatalogRegistration{
522		Datacenter: "dc1",
523		Node:       "foo",
524		ID:         "e0155642-135d-4739-9853-a1ee6c9f945b",
525		Address:    "127.0.0.2",
526		Checks: HealthChecks{
527			&HealthCheck{
528				Node:    "foo",
529				CheckID: "foo:alive",
530				Name:    "foo-liveness",
531				Status:  HealthPassing,
532				Notes:   "foo is alive and well",
533			},
534		},
535	}
536	catalog := c.Catalog()
537	if _, err := catalog.Register(&cr, nil); err != nil {
538		t.Fatalf("err: %v", err)
539	}
540
541	// If a custom node check is provided, it should overwrite serfHealth default
542	se.Node = "foo"
543	se.NodeChecks = []string{"foo:alive"}
544
545	id, _, err = session.Create(&se, nil)
546	if err != nil {
547		t.Fatalf("err: %v", err)
548	}
549	defer session.Destroy(id, nil)
550
551	info, qm, err = session.Info(id, nil)
552	if err != nil {
553		t.Fatalf("err: %v", err)
554	}
555	if qm.LastIndex == 0 {
556		t.Fatalf("bad: %v", qm)
557	}
558	if !qm.KnownLeader {
559		t.Fatalf("bad: %v", qm)
560	}
561	if info.CreateIndex == 0 {
562		t.Fatalf("bad: %v", info)
563	}
564	info.CreateIndex = 0
565
566	want = &SessionEntry{
567		ID:         id,
568		Node:       "foo",
569		NodeChecks: []string{"foo:alive"},
570		LockDelay:  15 * time.Second,
571		Behavior:   SessionBehaviorRelease,
572	}
573	want.Namespace = info.Namespace
574	assert.Equal(t, want, info)
575}
576
577func TestAPI_SessionServiceChecks(t *testing.T) {
578	t.Parallel()
579	c, s := makeClient(t)
580	defer s.Stop()
581
582	s.WaitForSerfCheck(t)
583
584	// Node check that doesn't exist should yield error on creation
585	se := SessionEntry{
586		ServiceChecks: []ServiceCheck{
587			{"dne", ""},
588		},
589	}
590	session := c.Session()
591
592	_, _, err := session.Create(&se, nil)
593	if err == nil {
594		t.Fatalf("should have failed")
595	}
596
597	// Register a new service with a check
598	cr := CatalogRegistration{
599		Datacenter:     "dc1",
600		Node:           s.Config.NodeName,
601		SkipNodeUpdate: true,
602		Service: &AgentService{
603			Kind:    ServiceKindTypical,
604			ID:      "redisV2",
605			Service: "redis",
606			Port:    1235,
607			Address: "198.18.1.2",
608		},
609		Checks: HealthChecks{
610			&HealthCheck{
611				Node:      s.Config.NodeName,
612				CheckID:   "redis:alive",
613				Status:    HealthPassing,
614				ServiceID: "redisV2",
615			},
616		},
617	}
618	catalog := c.Catalog()
619	if _, err := catalog.Register(&cr, nil); err != nil {
620		t.Fatalf("err: %v", err)
621	}
622
623	// If a custom check is provided, it should be present in session info
624	se.ServiceChecks = []ServiceCheck{
625		{"redis:alive", ""},
626	}
627
628	id, _, err := session.Create(&se, nil)
629	if err != nil {
630		t.Fatalf("err: %v", err)
631	}
632	defer session.Destroy(id, nil)
633
634	info, qm, err := session.Info(id, nil)
635	if err != nil {
636		t.Fatalf("err: %v", err)
637	}
638	if qm.LastIndex == 0 {
639		t.Fatalf("bad: %v", qm)
640	}
641	if !qm.KnownLeader {
642		t.Fatalf("bad: %v", qm)
643	}
644	if info.CreateIndex == 0 {
645		t.Fatalf("bad: %v", info)
646	}
647	info.CreateIndex = 0
648
649	want := &SessionEntry{
650		ID:            id,
651		Node:          s.Config.NodeName,
652		ServiceChecks: []ServiceCheck{{"redis:alive", ""}},
653		NodeChecks:    []string{"serfHealth"},
654		LockDelay:     15 * time.Second,
655		Behavior:      SessionBehaviorRelease,
656	}
657	want.Namespace = info.Namespace
658	assert.Equal(t, want, info)
659}
660