1//  Copyright (c) 2014 Couchbase, Inc.
2//  Licensed under the Apache License, Version 2.0 (the "License");
3//  you may not use this file except in compliance with the
4//  License. You may obtain a copy of the License at
5//    http://www.apache.org/licenses/LICENSE-2.0
6//  Unless required by applicable law or agreed to in writing,
7//  software distributed under the License is distributed on an "AS
8//  IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
9//  express or implied. See the License for the specific language
10//  governing permissions and limitations under the License.
11
12package rest
13
14import (
15	"bytes"
16	"io/ioutil"
17	"net/http"
18	"net/http/httptest"
19	"net/url"
20	"os"
21	"testing"
22
23	"github.com/gorilla/mux"
24
25	"github.com/couchbase/cbgt"
26)
27
28func TestInitStaticRouter(t *testing.T) {
29	r := mux.NewRouter()
30
31	staticDir := ""
32	staticETag := ""
33	pagesHandler := RewriteURL("/", http.FileServer(AssetFS()))
34
35	r = InitStaticRouter(r,
36		staticDir, staticETag, []string{
37			"/indexes",
38			"/nodes",
39			"/monitor",
40			"/manage",
41			"/logs",
42			"/debug",
43		}, pagesHandler)
44	if r == nil {
45		t.Errorf("expected r")
46	}
47}
48
49func TestMustEncode(t *testing.T) {
50	defer func() {
51		r := recover()
52		if r != nil {
53			t.Errorf("expected must encode to not panic anymore")
54		}
55	}()
56	MustEncode(&bytes.Buffer{}, func() {})
57}
58
59// Implements ManagerEventHandlers interface.
60type TestMEH struct {
61	lastPIndex *cbgt.PIndex
62	lastCall   string
63	ch         chan bool
64}
65
66func (meh *TestMEH) OnRegisterPIndex(pindex *cbgt.PIndex) {
67	meh.lastPIndex = pindex
68	meh.lastCall = "OnRegisterPIndex"
69	if meh.ch != nil {
70		meh.ch <- true
71	}
72}
73
74func (meh *TestMEH) OnUnregisterPIndex(pindex *cbgt.PIndex) {
75	meh.lastPIndex = pindex
76	meh.lastCall = "OnUnregisterPIndex"
77	if meh.ch != nil {
78		meh.ch <- true
79	}
80}
81
82func (meh *TestMEH) OnFeedError(srcType string, r cbgt.Feed, err error) {
83}
84
85func TestNewRESTRouter(t *testing.T) {
86	emptyDir, _ := ioutil.TempDir("./tmp", "test")
87	defer os.RemoveAll(emptyDir)
88
89	ring, err := cbgt.NewMsgRing(os.Stderr, 1000)
90	if err != nil {
91		t.Errorf("expected no ring errors")
92	}
93
94	cfg := cbgt.NewCfgMem()
95	mgr := cbgt.NewManager(cbgt.VERSION, cfg, cbgt.NewUUID(),
96		nil, "", 1, "", ":1000",
97		emptyDir, "some-datasource", nil)
98	r, meta, err := NewRESTRouter("v0", mgr, emptyDir, "", ring,
99		AssetDir, Asset)
100	if r == nil || meta == nil || err != nil {
101		t.Errorf("expected no errors")
102	}
103
104	mgr = cbgt.NewManager(cbgt.VERSION, cfg, cbgt.NewUUID(),
105		[]string{"queryer", "anotherTag"},
106		"", 1, "", ":1000", emptyDir, "some-datasource", nil)
107	r, meta, err = NewRESTRouter("v0", mgr, emptyDir, "", ring,
108		AssetDir, Asset)
109	if r == nil || meta == nil || err != nil {
110		t.Errorf("expected no errors")
111	}
112}
113
114type RESTHandlerTest struct {
115	Desc          string
116	Path          string
117	Method        string
118	Params        url.Values
119	Body          []byte
120	Status        int
121	ResponseBody  []byte
122	ResponseMatch map[string]bool
123
124	Before func()
125	After  func()
126}
127
128func (test *RESTHandlerTest) check(t *testing.T,
129	record *httptest.ResponseRecorder) {
130	desc := test.Desc
131	if desc == "" {
132		desc = test.Path + " " + test.Method
133	}
134
135	if got, want := record.Code, test.Status; got != want {
136		t.Errorf("%s: response code = %d, want %d", desc, got, want)
137		t.Errorf("%s: response body = %s", desc, record.Body)
138	}
139	got := bytes.TrimRight(record.Body.Bytes(), "\n")
140	if test.ResponseBody != nil {
141		if !bytes.Contains(got, test.ResponseBody) {
142			t.Errorf("%s: expected: '%s', got: '%s'",
143				desc, test.ResponseBody, got)
144		}
145	}
146	for pattern, shouldMatch := range test.ResponseMatch {
147		didMatch := bytes.Contains(got, []byte(pattern))
148		if didMatch != shouldMatch {
149			t.Errorf("%s: expected match %t for pattern %s, got %t",
150				desc, shouldMatch, pattern, didMatch)
151			t.Errorf("%s: response body was: %s", desc, got)
152		}
153	}
154}
155
156func testRESTHandlers(t *testing.T,
157	tests []*RESTHandlerTest, router *mux.Router) {
158	for _, test := range tests {
159		if test.Before != nil {
160			test.Before()
161		}
162		if test.Method != "NOOP" {
163			req := &http.Request{
164				Method: test.Method,
165				URL:    &url.URL{Path: test.Path},
166				Form:   test.Params,
167				Body:   ioutil.NopCloser(bytes.NewBuffer(test.Body)),
168			}
169			record := httptest.NewRecorder()
170			router.ServeHTTP(record, req)
171			test.check(t, record)
172		}
173		if test.After != nil {
174			test.After()
175		}
176	}
177}
178
179func TestHandlersForRuntimeOps(t *testing.T) {
180	emptyDir, err := ioutil.TempDir("./tmp", "test")
181	if err != nil {
182		t.Errorf("tempdir err: %v", err)
183	}
184	defer os.RemoveAll(emptyDir)
185
186	cfg := cbgt.NewCfgMem()
187	meh := &TestMEH{}
188	mgr := cbgt.NewManager(cbgt.VERSION, cfg, cbgt.NewUUID(),
189		nil, "", 1, "", ":1000", emptyDir, "some-datasource", meh)
190	err = mgr.Start("wanted")
191	if err != nil {
192		t.Errorf("expected no start err, got: %v", err)
193	}
194
195	mgr.Kick("test-start-kick")
196
197	mr, _ := cbgt.NewMsgRing(os.Stderr, 1000)
198	mr.Write([]byte("hello"))
199	mr.Write([]byte("world"))
200
201	router, _, err := NewRESTRouter("v0", mgr, "static", "", mr,
202		AssetDir, Asset)
203	if err != nil || router == nil {
204		t.Errorf("no mux router")
205	}
206
207	tests := []*RESTHandlerTest{
208		{
209			Path:   "/api/runtime",
210			Method: "GET",
211			Params: nil,
212			Body:   nil,
213			Status: http.StatusOK,
214			ResponseMatch: map[string]bool{
215				"arch":       true,
216				"go":         true,
217				"GOMAXPROCS": true,
218				"GOROOT":     true,
219			},
220		},
221		{
222			Path:          "/api/runtime/args",
223			Method:        "GET",
224			Params:        nil,
225			Body:          nil,
226			Status:        http.StatusOK,
227			ResponseMatch: map[string]bool{
228				// Actual production args are different from "go test" context.
229			},
230		},
231		{
232			Path:         "/api/runtime/gc",
233			Method:       "POST",
234			Params:       nil,
235			Body:         nil,
236			Status:       http.StatusOK,
237			ResponseBody: []byte(nil),
238		},
239		{
240			Path:   "/api/runtime/statsMem",
241			Method: "GET",
242			Params: nil,
243			Body:   nil,
244			Status: http.StatusOK,
245			ResponseMatch: map[string]bool{
246				"Alloc":      true,
247				"TotalAlloc": true,
248			},
249		},
250		{
251			Path:   "/api/runtime/profile/cpu",
252			Method: "POST",
253			Params: nil,
254			Body:   nil,
255			Status: 400,
256			ResponseMatch: map[string]bool{
257				"incorrect or missing secs parameter": true,
258			},
259		},
260		{
261			Path:   "/api/runtime/trace",
262			Method: "POST",
263			Params: nil,
264			Body:   nil,
265			Status: 400,
266			ResponseMatch: map[string]bool{
267				"incorrect or missing secs parameter": true,
268			},
269		},
270	}
271
272	testRESTHandlers(t, tests, router)
273}
274
275func TestHandlersForEmptyManager(t *testing.T) {
276	emptyDir, _ := ioutil.TempDir("./tmp", "test")
277	defer os.RemoveAll(emptyDir)
278
279	cfg := cbgt.NewCfgMem()
280	meh := &TestMEH{}
281	mgr := cbgt.NewManager(cbgt.VERSION, cfg, cbgt.NewUUID(),
282		nil, "", 1, "", ":1000", emptyDir, "some-datasource", meh)
283	err := mgr.Start("wanted")
284	if err != nil {
285		t.Errorf("expected start ok")
286	}
287	mgr.Kick("test-start-kick")
288
289	mr, _ := cbgt.NewMsgRing(os.Stderr, 1000)
290	mr.Write([]byte("hello"))
291	mr.Write([]byte("world"))
292
293	mgr.AddEvent([]byte(`"fizz"`))
294	mgr.AddEvent([]byte(`"buzz"`))
295
296	router, _, err := NewRESTRouter("v0", mgr, "static", "", mr,
297		AssetDir, Asset)
298	if err != nil || router == nil {
299		t.Errorf("no mux router")
300	}
301
302	tests := []*RESTHandlerTest{
303		{
304			Desc:         "log on empty msg ring",
305			Path:         "/api/log",
306			Method:       "GET",
307			Params:       nil,
308			Body:         nil,
309			Status:       http.StatusOK,
310			ResponseBody: []byte(`{"messages":["hello","world"],"events":["fizz","buzz"]}`),
311		},
312		{
313			Desc:   "cfg on empty manaager",
314			Path:   "/api/cfg",
315			Method: "GET",
316			Params: nil,
317			Body:   nil,
318			Status: http.StatusOK,
319			ResponseMatch: map[string]bool{
320				`"status":"ok"`:       true,
321				`"indexDefs":null`:    true,
322				`"nodeDefsKnown":{`:   true,
323				`"nodeDefsWanted":{`:  true,
324				`"planPIndexes":null`: true,
325			},
326		},
327		{
328			Desc:   "cfg refresh on empty, unchanged manager",
329			Path:   "/api/cfgRefresh",
330			Method: "POST",
331			Params: nil,
332			Body:   nil,
333			Status: http.StatusOK,
334			ResponseMatch: map[string]bool{
335				`{"status":"ok"}`: true,
336			},
337		},
338		{
339			Desc:         "Sets the given node definitions in Cfg",
340			Path:         "/api/cfgNodeDefs",
341			Method:       "PUT",
342			Params:       nil,
343			Body:         nil,
344			Status:       http.StatusBadRequest,
345			ResponseBody: []byte(`rest_manage: no request body found`),
346		},
347		{
348			Desc:         "Sets the given planPIndexes definitions in Cfg ",
349			Path:         "/api/cfgPlanPIndexes",
350			Method:       "PUT",
351			Params:       nil,
352			Body:         nil,
353			Status:       http.StatusBadRequest,
354			ResponseBody: []byte(`rest_manage: no request body found`),
355		},
356		{
357			Desc:   "manager kick on empty, unchanged manager",
358			Path:   "/api/managerKick",
359			Method: "POST",
360			Params: nil,
361			Body:   nil,
362			Status: http.StatusOK,
363			ResponseMatch: map[string]bool{
364				`{"status":"ok"}`: true,
365			},
366		},
367		{
368			Desc:   "manager meta",
369			Path:   "/api/managerMeta",
370			Method: "GET",
371			Params: nil,
372			Body:   nil,
373			Status: http.StatusOK,
374			ResponseMatch: map[string]bool{
375				`"status":"ok"`:    true,
376				`"startSamples":{`: true,
377			},
378		},
379		{
380			Desc:   "feed stats when no feeds",
381			Path:   "/api/stats",
382			Method: "GET",
383			Params: nil,
384			Body:   nil,
385			Status: http.StatusOK,
386			ResponseMatch: map[string]bool{
387				`{`: true,
388				`}`: true,
389			},
390		},
391		{
392			Desc:   "source partition seqs when no feeds",
393			Path:   "/api/stats/sourcePartitionSeqs/NOT-AN-INDEX",
394			Method: "GET",
395			Params: nil,
396			Body:   nil,
397			Status: 400,
398			ResponseMatch: map[string]bool{
399				`index not found`: true,
400			},
401		},
402		{
403			Desc:   "source stats when no feeds",
404			Path:   "/api/stats/sourceStats/NOT-AN-INDEX",
405			Method: "GET",
406			Params: nil,
407			Body:   nil,
408			Status: 400,
409			ResponseMatch: map[string]bool{
410				`index not found`: true,
411			},
412		},
413		{
414			Desc:         "list empty indexes",
415			Path:         "/api/index",
416			Method:       "GET",
417			Params:       nil,
418			Body:         nil,
419			Status:       http.StatusOK,
420			ResponseBody: []byte(`{"status":"ok","indexDefs":null}`),
421		},
422		{
423			Desc:         "try to get a nonexistent index",
424			Path:         "/api/index/NOT-AN-INDEX",
425			Method:       "GET",
426			Params:       nil,
427			Body:         nil,
428			Status:       400,
429			ResponseBody: []byte(`index not found`),
430		},
431		{
432			Desc:   "try to create a default index with no params",
433			Path:   "/api/index/index-on-a-bad-server",
434			Method: "PUT",
435			Params: nil,
436			Body:   nil,
437			Status: 400,
438			ResponseMatch: map[string]bool{
439				`rest_create_index: index type is required`: true,
440			},
441		},
442		{
443			Desc:   "try to create a default index with no sourceType",
444			Path:   "/api/index/index-on-a-bad-server",
445			Method: "PUT",
446			Params: url.Values{
447				"indexType": []string{"no-care"},
448			},
449			Body:   nil,
450			Status: 400,
451			ResponseMatch: map[string]bool{
452				`rest_create_index: sourceType is required`: true,
453			},
454		},
455		{
456			Desc:   "try to create a default index with bad indexType",
457			Path:   "/api/index/index-on-a-bad-server",
458			Method: "PUT",
459			Params: url.Values{
460				"indexType":  []string{"no-care"},
461				"sourceType": []string{"couchbase"},
462			},
463			Body:   nil,
464			Status: 400,
465			ResponseMatch: map[string]bool{
466				`unknown indexType`: true,
467			},
468		},
469		{
470			Desc:   "try to delete a nonexistent index when no indexes",
471			Path:   "/api/index/NOT-AN-INDEX",
472			Method: "DELETE",
473			Params: nil,
474			Body:   nil,
475			Status: 400,
476			ResponseMatch: map[string]bool{
477				`no indexes`: true,
478			},
479		},
480		{
481			Desc:   "try to count a nonexistent index when no indexes",
482			Path:   "/api/index/NOT-AN-INDEX/count",
483			Method: "GET",
484			Params: nil,
485			Body:   nil,
486			Status: 400,
487			ResponseMatch: map[string]bool{
488				`could not get indexDefs`: true,
489			},
490		},
491		{
492			Desc:   "try to query a nonexistent index when no indexes",
493			Path:   "/api/index/NOT-AN-INDEX/query",
494			Method: "POST",
495			Params: nil,
496			Body:   nil,
497			Status: 400,
498			ResponseMatch: map[string]bool{
499				`could not get indexDefs`: true,
500			},
501		},
502		{
503			Desc:   "create an index with bogus indexType",
504			Path:   "/api/index/idxBogusIndexType",
505			Method: "PUT",
506			Params: url.Values{
507				"indexType":  []string{"not-a-real-index-type"},
508				"sourceType": []string{"nil"},
509			},
510			Body:   nil,
511			Status: 400,
512			ResponseMatch: map[string]bool{
513				`error`: true,
514			},
515		},
516		{
517			Desc:   "create a blackhole index",
518			Path:   "/api/index/bh0",
519			Method: "PUT",
520			Params: url.Values{
521				"indexType":  []string{"blackhole"},
522				"sourceType": []string{"nil"},
523			},
524			Body:   nil,
525			Status: 200,
526		},
527		{
528			Desc:   "create a blackhole index via body",
529			Path:   "/api/index/bh1",
530			Method: "PUT",
531			Body:   []byte(`{"type":"blackhole","sourceType":"nil"}`),
532			Status: 200,
533		},
534		{
535			Desc:   "source partition seqs on bh1",
536			Path:   "/api/stats/sourcePartitionSeqs/bh1",
537			Method: "GET",
538			Params: nil,
539			Body:   nil,
540			Status: 200,
541			ResponseMatch: map[string]bool{
542				`null`: true,
543			},
544		},
545		{
546			Desc:   "source stats on bh1",
547			Path:   "/api/stats/sourceStats/bh1",
548			Method: "GET",
549			Params: nil,
550			Body:   nil,
551			Status: 200,
552			ResponseMatch: map[string]bool{
553				`null`: true,
554			},
555		},
556		{
557			Desc:   "create a blackhole index, bad params",
558			Path:   "/api/index/bh2",
559			Method: "PUT",
560			Body:   []byte(`{"type":"blackhole","params":666,"sourceType":"nil"}`),
561			Status: 400,
562			ResponseMatch: map[string]bool{
563				`cannot unmarshal number`: true,
564			},
565		},
566		{
567			Desc:   "create a blackhole index, bad params",
568			Path:   "/api/index/bh2",
569			Method: "PUT",
570			Body:   []byte(`{"type":"blackhole","params":"hi","sourceType":"nil"}`),
571			Status: 400,
572		},
573		{
574			Desc:   "create a blackhole index, params {} json",
575			Path:   "/api/index/bh3",
576			Method: "PUT",
577			Body:   []byte(`{"type":"blackhole","params":{},"sourceType":"nil"}`),
578			Status: 200,
579		},
580		{
581			Desc:   "create a blackhole index, bad sourceParams",
582			Path:   "/api/index/bh2s",
583			Method: "PUT",
584			Body:   []byte(`{"type":"blackhole","sourceParams":666,"sourceType":"nil"}`),
585			Status: 400,
586			ResponseMatch: map[string]bool{
587				`cannot unmarshal number`: true,
588			},
589		},
590		{
591			Desc:   "create a blackhole index, bad sourceParams",
592			Path:   "/api/index/bh2s",
593			Method: "PUT",
594			Body:   []byte(`{"type":"blackhole","sourceParams":"hi","sourceType":"nil"}`),
595			Status: 400,
596		},
597		{
598			Desc:   "create a blackhole index, sourceParams {} json",
599			Path:   "/api/index/bh3s",
600			Method: "PUT",
601			Body:   []byte(`{"type":"blackhole","sourceParams":{},"sourceType":"nil"}`),
602			Status: 200,
603		},
604		{
605			Desc:   "look up a pindexId for a non existent indexname and document ID",
606			Path:   "/api/index/idx/pindexLookup",
607			Method: "POST",
608			Params: url.Values{
609				"u": []string{"Administrator:asdasd"},
610			},
611			Body:   []byte(`{"docId":"blabla"}`),
612			Status: 404,
613			ResponseMatch: map[string]bool{
614				`manager: no indexDef, indexName: idx`: true,
615			},
616		},
617	}
618
619	testRESTHandlers(t, tests, router)
620}
621
622func TestPathFocusName(t *testing.T) {
623	tests := []struct {
624		inp string
625		exp string
626	}{
627		{"", ""},
628		{"hello", ""},
629		{"/api/index/{indexName}", "indexName"},
630		{"/api/index/{indexName}/query", "indexName"},
631	}
632
633	for testi, test := range tests {
634		got := PathFocusName(test.inp)
635		if got != test.exp {
636			t.Errorf("testi: %d, %s != %s on input %s",
637				testi, got, test.exp, test.inp)
638		}
639	}
640}
641