1// Copyright 2012 Google Inc. All rights reserved.
2// Use of this source code is governed by the Apache 2.0
3// license that can be found in the LICENSE file.
4
5package search
6
7import (
8	"errors"
9	"fmt"
10	"reflect"
11	"strings"
12	"testing"
13	"time"
14
15	"github.com/golang/protobuf/proto"
16
17	"google.golang.org/appengine"
18	"google.golang.org/appengine/internal/aetesting"
19	pb "google.golang.org/appengine/internal/search"
20)
21
22type TestDoc struct {
23	String   string
24	Atom     Atom
25	HTML     HTML
26	Float    float64
27	Location appengine.GeoPoint
28	Time     time.Time
29}
30
31type FieldListWithMeta struct {
32	Fields FieldList
33	Meta   *DocumentMetadata
34}
35
36func (f *FieldListWithMeta) Load(fields []Field, meta *DocumentMetadata) error {
37	f.Meta = meta
38	return f.Fields.Load(fields, nil)
39}
40
41func (f *FieldListWithMeta) Save() ([]Field, *DocumentMetadata, error) {
42	fields, _, err := f.Fields.Save()
43	return fields, f.Meta, err
44}
45
46// Assert that FieldListWithMeta satisfies FieldLoadSaver
47var _ FieldLoadSaver = &FieldListWithMeta{}
48
49var (
50	float       = 3.14159
51	floatOut    = "3.14159e+00"
52	latitude    = 37.3894
53	longitude   = 122.0819
54	testGeo     = appengine.GeoPoint{latitude, longitude}
55	testString  = "foo<b>bar"
56	testTime    = time.Unix(1337324400, 0)
57	testTimeOut = "1337324400000"
58	searchMeta  = &DocumentMetadata{
59		Rank: 42,
60	}
61	searchDoc = TestDoc{
62		String:   testString,
63		Atom:     Atom(testString),
64		HTML:     HTML(testString),
65		Float:    float,
66		Location: testGeo,
67		Time:     testTime,
68	}
69	searchFields = FieldList{
70		Field{Name: "String", Value: testString},
71		Field{Name: "Atom", Value: Atom(testString)},
72		Field{Name: "HTML", Value: HTML(testString)},
73		Field{Name: "Float", Value: float},
74		Field{Name: "Location", Value: testGeo},
75		Field{Name: "Time", Value: testTime},
76	}
77	// searchFieldsWithLang is a copy of the searchFields with the Language field
78	// set on text/HTML Fields.
79	searchFieldsWithLang = FieldList{}
80	protoFields          = []*pb.Field{
81		newStringValueField("String", testString, pb.FieldValue_TEXT),
82		newStringValueField("Atom", testString, pb.FieldValue_ATOM),
83		newStringValueField("HTML", testString, pb.FieldValue_HTML),
84		newStringValueField("Float", floatOut, pb.FieldValue_NUMBER),
85		{
86			Name: proto.String("Location"),
87			Value: &pb.FieldValue{
88				Geo: &pb.FieldValue_Geo{
89					Lat: proto.Float64(latitude),
90					Lng: proto.Float64(longitude),
91				},
92				Type: pb.FieldValue_GEO.Enum(),
93			},
94		},
95		newStringValueField("Time", testTimeOut, pb.FieldValue_DATE),
96	}
97)
98
99func init() {
100	for _, f := range searchFields {
101		if f.Name == "String" || f.Name == "HTML" {
102			f.Language = "en"
103		}
104		searchFieldsWithLang = append(searchFieldsWithLang, f)
105	}
106}
107
108func newStringValueField(name, value string, valueType pb.FieldValue_ContentType) *pb.Field {
109	return &pb.Field{
110		Name: proto.String(name),
111		Value: &pb.FieldValue{
112			StringValue: proto.String(value),
113			Type:        valueType.Enum(),
114		},
115	}
116}
117
118func newFacet(name, value string, valueType pb.FacetValue_ContentType) *pb.Facet {
119	return &pb.Facet{
120		Name: proto.String(name),
121		Value: &pb.FacetValue{
122			StringValue: proto.String(value),
123			Type:        valueType.Enum(),
124		},
125	}
126}
127
128func TestValidIndexNameOrDocID(t *testing.T) {
129	testCases := []struct {
130		s    string
131		want bool
132	}{
133		{"", true},
134		{"!", false},
135		{"$", true},
136		{"!bad", false},
137		{"good!", true},
138		{"alsoGood", true},
139		{"has spaces", false},
140		{"is_inva\xffid_UTF-8", false},
141		{"is_non-ASCïI", false},
142		{"underscores_are_ok", true},
143	}
144	for _, tc := range testCases {
145		if got := validIndexNameOrDocID(tc.s); got != tc.want {
146			t.Errorf("%q: got %v, want %v", tc.s, got, tc.want)
147		}
148	}
149}
150
151func TestLoadDoc(t *testing.T) {
152	got, want := TestDoc{}, searchDoc
153	if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
154		t.Fatalf("loadDoc: %v", err)
155	}
156	if got != want {
157		t.Errorf("loadDoc: got %v, wanted %v", got, want)
158	}
159}
160
161func TestSaveDoc(t *testing.T) {
162	got, err := saveDoc(&searchDoc)
163	if err != nil {
164		t.Fatalf("saveDoc: %v", err)
165	}
166	want := protoFields
167	if !reflect.DeepEqual(got.Field, want) {
168		t.Errorf("\ngot  %v\nwant %v", got, want)
169	}
170}
171
172func TestSaveDocUsesDefaultedRankIfNotSpecified(t *testing.T) {
173	got, err := saveDoc(&searchDoc)
174	if err != nil {
175		t.Fatalf("saveDoc: %v", err)
176	}
177	orderIdSource := got.GetOrderIdSource()
178	if orderIdSource != pb.Document_DEFAULTED {
179		t.Errorf("OrderIdSource: got %v, wanted DEFAULTED", orderIdSource)
180	}
181}
182
183func TestLoadFieldList(t *testing.T) {
184	var got FieldList
185	want := searchFieldsWithLang
186	if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
187		t.Fatalf("loadDoc: %v", err)
188	}
189	if !reflect.DeepEqual(got, want) {
190		t.Errorf("\ngot  %v\nwant %v", got, want)
191	}
192}
193
194func TestLangFields(t *testing.T) {
195	fl := &FieldList{
196		{Name: "Foo", Value: "I am English", Language: "en"},
197		{Name: "Bar", Value: "私は日本人だ", Language: "ja"},
198	}
199	var got FieldList
200	doc, err := saveDoc(fl)
201	if err != nil {
202		t.Fatalf("saveDoc: %v", err)
203	}
204	if err := loadDoc(&got, doc, nil); err != nil {
205		t.Fatalf("loadDoc: %v", err)
206	}
207	if want := fl; !reflect.DeepEqual(&got, want) {
208		t.Errorf("got  %v\nwant %v", got, want)
209	}
210}
211
212func TestSaveFieldList(t *testing.T) {
213	got, err := saveDoc(&searchFields)
214	if err != nil {
215		t.Fatalf("saveDoc: %v", err)
216	}
217	want := protoFields
218	if !reflect.DeepEqual(got.Field, want) {
219		t.Errorf("\ngot  %v\nwant %v", got, want)
220	}
221}
222
223func TestLoadFieldAndExprList(t *testing.T) {
224	var got, want FieldList
225	for i, f := range searchFieldsWithLang {
226		f.Derived = (i >= 2) // First 2 elements are "fields", next are "expressions".
227		want = append(want, f)
228	}
229	doc, expr := &pb.Document{Field: protoFields[:2]}, protoFields[2:]
230	if err := loadDoc(&got, doc, expr); err != nil {
231		t.Fatalf("loadDoc: %v", err)
232	}
233	if !reflect.DeepEqual(got, want) {
234		t.Errorf("got  %v\nwant %v", got, want)
235	}
236}
237
238func TestLoadMeta(t *testing.T) {
239	var got FieldListWithMeta
240	want := FieldListWithMeta{
241		Meta:   searchMeta,
242		Fields: searchFieldsWithLang,
243	}
244	doc := &pb.Document{
245		Field:         protoFields,
246		OrderId:       proto.Int32(42),
247		OrderIdSource: pb.Document_SUPPLIED.Enum(),
248	}
249	if err := loadDoc(&got, doc, nil); err != nil {
250		t.Fatalf("loadDoc: %v", err)
251	}
252	if !reflect.DeepEqual(got, want) {
253		t.Errorf("\ngot  %v\nwant %v", got, want)
254	}
255}
256
257func TestSaveMeta(t *testing.T) {
258	got, err := saveDoc(&FieldListWithMeta{
259		Meta:   searchMeta,
260		Fields: searchFields,
261	})
262	if err != nil {
263		t.Fatalf("saveDoc: %v", err)
264	}
265	want := &pb.Document{
266		Field:         protoFields,
267		OrderId:       proto.Int32(42),
268		OrderIdSource: pb.Document_SUPPLIED.Enum(),
269	}
270	if !proto.Equal(got, want) {
271		t.Errorf("\ngot  %v\nwant %v", got, want)
272	}
273}
274
275func TestSaveMetaWithDefaultedRank(t *testing.T) {
276	metaWithoutRank := &DocumentMetadata{
277		Rank: 0,
278	}
279	got, err := saveDoc(&FieldListWithMeta{
280		Meta:   metaWithoutRank,
281		Fields: searchFields,
282	})
283	if err != nil {
284		t.Fatalf("saveDoc: %v", err)
285	}
286	want := &pb.Document{
287		Field:         protoFields,
288		OrderId:       got.OrderId,
289		OrderIdSource: pb.Document_DEFAULTED.Enum(),
290	}
291	if !proto.Equal(got, want) {
292		t.Errorf("\ngot  %v\nwant %v", got, want)
293	}
294}
295
296func TestSaveWithoutMetaUsesDefaultedRank(t *testing.T) {
297	got, err := saveDoc(&FieldListWithMeta{
298		Fields: searchFields,
299	})
300	if err != nil {
301		t.Fatalf("saveDoc: %v", err)
302	}
303	want := &pb.Document{
304		Field:         protoFields,
305		OrderId:       got.OrderId,
306		OrderIdSource: pb.Document_DEFAULTED.Enum(),
307	}
308	if !proto.Equal(got, want) {
309		t.Errorf("\ngot  %v\nwant %v", got, want)
310	}
311}
312
313func TestLoadSaveWithStruct(t *testing.T) {
314	type gopher struct {
315		Name string
316		Info string  `search:"about"`
317		Legs float64 `search:",facet"`
318		Fuzz Atom    `search:"Fur,facet"`
319	}
320
321	doc := gopher{"Gopher", "Likes slide rules.", 4, Atom("furry")}
322	pb := &pb.Document{
323		Field: []*pb.Field{
324			newStringValueField("Name", "Gopher", pb.FieldValue_TEXT),
325			newStringValueField("about", "Likes slide rules.", pb.FieldValue_TEXT),
326		},
327		Facet: []*pb.Facet{
328			newFacet("Legs", "4e+00", pb.FacetValue_NUMBER),
329			newFacet("Fur", "furry", pb.FacetValue_ATOM),
330		},
331	}
332
333	var gotDoc gopher
334	if err := loadDoc(&gotDoc, pb, nil); err != nil {
335		t.Fatalf("loadDoc: %v", err)
336	}
337	if !reflect.DeepEqual(gotDoc, doc) {
338		t.Errorf("loading doc\ngot  %v\nwant %v", gotDoc, doc)
339	}
340
341	gotPB, err := saveDoc(&doc)
342	if err != nil {
343		t.Fatalf("saveDoc: %v", err)
344	}
345	gotPB.OrderId = nil       // Don't test: it's time dependent.
346	gotPB.OrderIdSource = nil // Don't test because it's contingent on OrderId.
347	if !proto.Equal(gotPB, pb) {
348		t.Errorf("saving doc\ngot  %v\nwant %v", gotPB, pb)
349	}
350}
351
352func TestValidFieldNames(t *testing.T) {
353	testCases := []struct {
354		name  string
355		valid bool
356	}{
357		{"Normal", true},
358		{"Also_OK_123", true},
359		{"Not so great", false},
360		{"lower_case", true},
361		{"Exclaim!", false},
362		{"Hello세상아 안녕", false},
363		{"", false},
364		{"Hεllo", false},
365		{strings.Repeat("A", 500), true},
366		{strings.Repeat("A", 501), false},
367	}
368
369	for _, tc := range testCases {
370		_, err := saveDoc(&FieldList{
371			Field{Name: tc.name, Value: "val"},
372		})
373		if err != nil && !strings.Contains(err.Error(), "invalid field name") {
374			t.Errorf("unexpected err %q for field name %q", err, tc.name)
375		}
376		if (err == nil) != tc.valid {
377			t.Errorf("field %q: expected valid %t, received err %v", tc.name, tc.valid, err)
378		}
379	}
380}
381
382func TestValidLangs(t *testing.T) {
383	testCases := []struct {
384		field Field
385		valid bool
386	}{
387		{Field{Name: "Foo", Value: "String", Language: ""}, true},
388		{Field{Name: "Foo", Value: "String", Language: "en"}, true},
389		{Field{Name: "Foo", Value: "String", Language: "aussie"}, false},
390		{Field{Name: "Foo", Value: "String", Language: "12"}, false},
391		{Field{Name: "Foo", Value: HTML("String"), Language: "en"}, true},
392		{Field{Name: "Foo", Value: Atom("String"), Language: "en"}, false},
393		{Field{Name: "Foo", Value: 42, Language: "en"}, false},
394	}
395
396	for _, tt := range testCases {
397		_, err := saveDoc(&FieldList{tt.field})
398		if err == nil != tt.valid {
399			t.Errorf("Field %v, got error %v, wanted valid %t", tt.field, err, tt.valid)
400		}
401	}
402}
403
404func TestDuplicateFields(t *testing.T) {
405	testCases := []struct {
406		desc   string
407		fields FieldList
408		errMsg string // Non-empty if we expect an error
409	}{
410		{
411			desc:   "multi string",
412			fields: FieldList{{Name: "FieldA", Value: "val1"}, {Name: "FieldA", Value: "val2"}, {Name: "FieldA", Value: "val3"}},
413		},
414		{
415			desc:   "multi atom",
416			fields: FieldList{{Name: "FieldA", Value: Atom("val1")}, {Name: "FieldA", Value: Atom("val2")}, {Name: "FieldA", Value: Atom("val3")}},
417		},
418		{
419			desc:   "mixed",
420			fields: FieldList{{Name: "FieldA", Value: testString}, {Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: float}},
421		},
422		{
423			desc:   "multi time",
424			fields: FieldList{{Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: testTime}},
425			errMsg: `duplicate time field "FieldA"`,
426		},
427		{
428			desc:   "multi num",
429			fields: FieldList{{Name: "FieldA", Value: float}, {Name: "FieldA", Value: float}},
430			errMsg: `duplicate numeric field "FieldA"`,
431		},
432	}
433	for _, tc := range testCases {
434		_, err := saveDoc(&tc.fields)
435		if (err == nil) != (tc.errMsg == "") || (err != nil && !strings.Contains(err.Error(), tc.errMsg)) {
436			t.Errorf("%s: got err %v, wanted %q", tc.desc, err, tc.errMsg)
437		}
438	}
439}
440
441func TestLoadErrFieldMismatch(t *testing.T) {
442	testCases := []struct {
443		desc string
444		dst  interface{}
445		src  []*pb.Field
446		err  error
447	}{
448		{
449			desc: "missing",
450			dst:  &struct{ One string }{},
451			src:  []*pb.Field{newStringValueField("Two", "woop!", pb.FieldValue_TEXT)},
452			err: &ErrFieldMismatch{
453				FieldName: "Two",
454				Reason:    "no such struct field",
455			},
456		},
457		{
458			desc: "wrong type",
459			dst:  &struct{ Num float64 }{},
460			src:  []*pb.Field{newStringValueField("Num", "woop!", pb.FieldValue_TEXT)},
461			err: &ErrFieldMismatch{
462				FieldName: "Num",
463				Reason:    "type mismatch: float64 for string data",
464			},
465		},
466		{
467			desc: "unsettable",
468			dst:  &struct{ lower string }{},
469			src:  []*pb.Field{newStringValueField("lower", "woop!", pb.FieldValue_TEXT)},
470			err: &ErrFieldMismatch{
471				FieldName: "lower",
472				Reason:    "cannot set struct field",
473			},
474		},
475	}
476	for _, tc := range testCases {
477		err := loadDoc(tc.dst, &pb.Document{Field: tc.src}, nil)
478		if !reflect.DeepEqual(err, tc.err) {
479			t.Errorf("%s, got err %v, wanted %v", tc.desc, err, tc.err)
480		}
481	}
482}
483
484func TestLimit(t *testing.T) {
485	index, err := Open("Doc")
486	if err != nil {
487		t.Fatalf("err from Open: %v", err)
488	}
489	c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, res *pb.SearchResponse) error {
490		limit := 20 // Default per page.
491		if req.Params.Limit != nil {
492			limit = int(*req.Params.Limit)
493		}
494		res.Status = &pb.RequestStatus{Code: pb.SearchServiceError_OK.Enum()}
495		res.MatchedCount = proto.Int64(int64(limit))
496		for i := 0; i < limit; i++ {
497			res.Result = append(res.Result, &pb.SearchResult{Document: &pb.Document{}})
498			res.Cursor = proto.String("moreresults")
499		}
500		return nil
501	})
502
503	const maxDocs = 500 // Limit maximum number of docs.
504	testCases := []struct {
505		limit, want int
506	}{
507		{limit: 0, want: maxDocs},
508		{limit: 42, want: 42},
509		{limit: 100, want: 100},
510		{limit: 1000, want: maxDocs},
511	}
512
513	for _, tt := range testCases {
514		it := index.Search(c, "gopher", &SearchOptions{Limit: tt.limit, IDsOnly: true})
515		count := 0
516		for ; count < maxDocs; count++ {
517			_, err := it.Next(nil)
518			if err == Done {
519				break
520			}
521			if err != nil {
522				t.Fatalf("err after %d: %v", count, err)
523			}
524		}
525		if count != tt.want {
526			t.Errorf("got %d results, expected %d", count, tt.want)
527		}
528	}
529}
530
531func TestPut(t *testing.T) {
532	index, err := Open("Doc")
533	if err != nil {
534		t.Fatalf("err from Open: %v", err)
535	}
536
537	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
538		expectedIn := &pb.IndexDocumentRequest{
539			Params: &pb.IndexDocumentParams{
540				Document: []*pb.Document{
541					{Field: protoFields, OrderId: proto.Int32(42), OrderIdSource: pb.Document_SUPPLIED.Enum()},
542				},
543				IndexSpec: &pb.IndexSpec{
544					Name: proto.String("Doc"),
545				},
546			},
547		}
548		if !proto.Equal(in, expectedIn) {
549			return fmt.Errorf("unsupported argument:\ngot  %v\nwant %v", in, expectedIn)
550		}
551		*out = pb.IndexDocumentResponse{
552			Status: []*pb.RequestStatus{
553				{Code: pb.SearchServiceError_OK.Enum()},
554			},
555			DocId: []string{
556				"doc_id",
557			},
558		}
559		return nil
560	})
561
562	id, err := index.Put(c, "", &FieldListWithMeta{
563		Meta:   searchMeta,
564		Fields: searchFields,
565	})
566	if err != nil {
567		t.Fatal(err)
568	}
569	if want := "doc_id"; id != want {
570		t.Errorf("Got doc ID %q, want %q", id, want)
571	}
572}
573
574func TestPutAutoOrderID(t *testing.T) {
575	index, err := Open("Doc")
576	if err != nil {
577		t.Fatalf("err from Open: %v", err)
578	}
579
580	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
581		if len(in.Params.GetDocument()) < 1 {
582			return fmt.Errorf("expected at least one Document, got %v", in)
583		}
584		got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds())
585		if d := got - want; -5 > d || d > 5 {
586			return fmt.Errorf("got OrderId %d, want near %d", got, want)
587		}
588		*out = pb.IndexDocumentResponse{
589			Status: []*pb.RequestStatus{
590				{Code: pb.SearchServiceError_OK.Enum()},
591			},
592			DocId: []string{
593				"doc_id",
594			},
595		}
596		return nil
597	})
598
599	if _, err := index.Put(c, "", &searchFields); err != nil {
600		t.Fatal(err)
601	}
602}
603
604func TestPutBadStatus(t *testing.T) {
605	index, err := Open("Doc")
606	if err != nil {
607		t.Fatalf("err from Open: %v", err)
608	}
609
610	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(_ *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
611		*out = pb.IndexDocumentResponse{
612			Status: []*pb.RequestStatus{
613				{
614					Code:        pb.SearchServiceError_INVALID_REQUEST.Enum(),
615					ErrorDetail: proto.String("insufficient gophers"),
616				},
617			},
618		}
619		return nil
620	})
621
622	wantErr := "search: INVALID_REQUEST: insufficient gophers"
623	if _, err := index.Put(c, "", &searchFields); err == nil || err.Error() != wantErr {
624		t.Fatalf("Put: got %v error, want %q", err, wantErr)
625	}
626}
627
628func TestPutMultiNilIDSlice(t *testing.T) {
629	index, err := Open("Doc")
630	if err != nil {
631		t.Fatalf("err from Open: %v", err)
632	}
633
634	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
635		if len(in.Params.GetDocument()) < 1 {
636			return fmt.Errorf("got %v, want at least 1 document", in)
637		}
638		got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds())
639		if d := got - want; -5 > d || d > 5 {
640			return fmt.Errorf("got OrderId %d, want near %d", got, want)
641		}
642		*out = pb.IndexDocumentResponse{
643			Status: []*pb.RequestStatus{
644				{Code: pb.SearchServiceError_OK.Enum()},
645			},
646			DocId: []string{
647				"doc_id",
648			},
649		}
650		return nil
651	})
652
653	if _, err := index.PutMulti(c, nil, []interface{}{&searchFields}); err != nil {
654		t.Fatal(err)
655	}
656}
657
658func TestPutMultiError(t *testing.T) {
659	index, err := Open("Doc")
660	if err != nil {
661		t.Fatalf("err from Open: %v", err)
662	}
663
664	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
665		*out = pb.IndexDocumentResponse{
666			Status: []*pb.RequestStatus{
667				{Code: pb.SearchServiceError_OK.Enum()},
668				{Code: pb.SearchServiceError_PERMISSION_DENIED.Enum(), ErrorDetail: proto.String("foo")},
669			},
670			DocId: []string{
671				"id1",
672				"",
673			},
674		}
675		return nil
676	})
677
678	switch _, err := index.PutMulti(c, nil, []interface{}{&searchFields, &searchFields}); {
679	case err == nil:
680		t.Fatalf("got nil, want error")
681	case err.(appengine.MultiError)[0] != nil:
682		t.Fatalf("got %v, want nil MultiError[0]", err.(appengine.MultiError)[0])
683	case err.(appengine.MultiError)[1] == nil:
684		t.Fatalf("got nil, want not-nill MultiError[1]")
685	}
686}
687
688func TestPutMultiWrongNumberOfIDs(t *testing.T) {
689	index, err := Open("Doc")
690	if err != nil {
691		t.Fatalf("err from Open: %v", err)
692	}
693
694	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
695		return nil
696	})
697
698	if _, err := index.PutMulti(c, []string{"a"}, []interface{}{&searchFields, &searchFields}); err == nil {
699		t.Fatal("got success, want error")
700	}
701}
702
703func TestPutMultiTooManyDocs(t *testing.T) {
704	index, err := Open("Doc")
705	if err != nil {
706		t.Fatalf("err from Open: %v", err)
707	}
708
709	c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
710		return nil
711	})
712
713	srcs := make([]interface{}, 201)
714	for i, _ := range srcs {
715		srcs[i] = &searchFields
716	}
717
718	if _, err := index.PutMulti(c, nil, srcs); err != ErrTooManyDocuments {
719		t.Fatalf("got %v, want ErrTooManyDocuments", err)
720	}
721}
722
723func TestSortOptions(t *testing.T) {
724	index, err := Open("Doc")
725	if err != nil {
726		t.Fatalf("err from Open: %v", err)
727	}
728
729	noErr := errors.New("") // Sentinel err to return to prevent sending request.
730
731	testCases := []struct {
732		desc       string
733		sort       *SortOptions
734		wantSort   []*pb.SortSpec
735		wantScorer *pb.ScorerSpec
736		wantErr    string
737	}{
738		{
739			desc: "No SortOptions",
740		},
741		{
742			desc: "Basic",
743			sort: &SortOptions{
744				Expressions: []SortExpression{
745					{Expr: "dog"},
746					{Expr: "cat", Reverse: true},
747					{Expr: "gopher", Default: "blue"},
748					{Expr: "fish", Default: 2.0},
749				},
750				Limit:  42,
751				Scorer: MatchScorer,
752			},
753			wantSort: []*pb.SortSpec{
754				{SortExpression: proto.String("dog")},
755				{SortExpression: proto.String("cat"), SortDescending: proto.Bool(false)},
756				{SortExpression: proto.String("gopher"), DefaultValueText: proto.String("blue")},
757				{SortExpression: proto.String("fish"), DefaultValueNumeric: proto.Float64(2)},
758			},
759			wantScorer: &pb.ScorerSpec{
760				Limit:  proto.Int32(42),
761				Scorer: pb.ScorerSpec_MATCH_SCORER.Enum(),
762			},
763		},
764		{
765			desc: "Bad expression default",
766			sort: &SortOptions{
767				Expressions: []SortExpression{
768					{Expr: "dog", Default: true},
769				},
770			},
771			wantErr: `search: invalid Default type bool for expression "dog"`,
772		},
773		{
774			desc:       "RescoringMatchScorer",
775			sort:       &SortOptions{Scorer: RescoringMatchScorer},
776			wantScorer: &pb.ScorerSpec{Scorer: pb.ScorerSpec_RESCORING_MATCH_SCORER.Enum()},
777		},
778	}
779
780	for _, tt := range testCases {
781		c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
782			params := req.Params
783			if !reflect.DeepEqual(params.SortSpec, tt.wantSort) {
784				t.Errorf("%s: params.SortSpec=%v; want %v", tt.desc, params.SortSpec, tt.wantSort)
785			}
786			if !reflect.DeepEqual(params.ScorerSpec, tt.wantScorer) {
787				t.Errorf("%s: params.ScorerSpec=%v; want %v", tt.desc, params.ScorerSpec, tt.wantScorer)
788			}
789			return noErr // Always return some error to prevent response parsing.
790		})
791
792		it := index.Search(c, "gopher", &SearchOptions{Sort: tt.sort})
793		_, err := it.Next(nil)
794		if err == nil {
795			t.Fatalf("%s: err==nil; should not happen", tt.desc)
796		}
797		if err.Error() != tt.wantErr {
798			t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
799		}
800	}
801}
802
803func TestFieldSpec(t *testing.T) {
804	index, err := Open("Doc")
805	if err != nil {
806		t.Fatalf("err from Open: %v", err)
807	}
808
809	errFoo := errors.New("foo") // sentinel error when there isn't one.
810
811	testCases := []struct {
812		desc string
813		opts *SearchOptions
814		want *pb.FieldSpec
815	}{
816		{
817			desc: "No options",
818			want: &pb.FieldSpec{},
819		},
820		{
821			desc: "Fields",
822			opts: &SearchOptions{
823				Fields: []string{"one", "two"},
824			},
825			want: &pb.FieldSpec{
826				Name: []string{"one", "two"},
827			},
828		},
829		{
830			desc: "Expressions",
831			opts: &SearchOptions{
832				Expressions: []FieldExpression{
833					{Name: "one", Expr: "price * quantity"},
834					{Name: "two", Expr: "min(daily_use, 10) * rate"},
835				},
836			},
837			want: &pb.FieldSpec{
838				Expression: []*pb.FieldSpec_Expression{
839					{Name: proto.String("one"), Expression: proto.String("price * quantity")},
840					{Name: proto.String("two"), Expression: proto.String("min(daily_use, 10) * rate")},
841				},
842			},
843		},
844	}
845
846	for _, tt := range testCases {
847		c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
848			params := req.Params
849			if !reflect.DeepEqual(params.FieldSpec, tt.want) {
850				t.Errorf("%s: params.FieldSpec=%v; want %v", tt.desc, params.FieldSpec, tt.want)
851			}
852			return errFoo // Always return some error to prevent response parsing.
853		})
854
855		it := index.Search(c, "gopher", tt.opts)
856		if _, err := it.Next(nil); err != errFoo {
857			t.Fatalf("%s: got error %v; want %v", tt.desc, err, errFoo)
858		}
859	}
860}
861
862func TestBasicSearchOpts(t *testing.T) {
863	index, err := Open("Doc")
864	if err != nil {
865		t.Fatalf("err from Open: %v", err)
866	}
867
868	noErr := errors.New("") // Sentinel err to return to prevent sending request.
869
870	testCases := []struct {
871		desc          string
872		facetOpts     []FacetSearchOption
873		cursor        Cursor
874		offset        int
875		countAccuracy int
876		want          *pb.SearchParams
877		wantErr       string
878	}{
879		{
880			desc: "No options",
881			want: &pb.SearchParams{},
882		},
883		{
884			desc: "Default auto discovery",
885			facetOpts: []FacetSearchOption{
886				AutoFacetDiscovery(0, 0),
887			},
888			want: &pb.SearchParams{
889				AutoDiscoverFacetCount: proto.Int32(10),
890			},
891		},
892		{
893			desc: "Auto discovery",
894			facetOpts: []FacetSearchOption{
895				AutoFacetDiscovery(7, 12),
896			},
897			want: &pb.SearchParams{
898				AutoDiscoverFacetCount: proto.Int32(7),
899				FacetAutoDetectParam: &pb.FacetAutoDetectParam{
900					ValueLimit: proto.Int32(12),
901				},
902			},
903		},
904		{
905			desc: "Param Depth",
906			facetOpts: []FacetSearchOption{
907				AutoFacetDiscovery(7, 12),
908			},
909			want: &pb.SearchParams{
910				AutoDiscoverFacetCount: proto.Int32(7),
911				FacetAutoDetectParam: &pb.FacetAutoDetectParam{
912					ValueLimit: proto.Int32(12),
913				},
914			},
915		},
916		{
917			desc: "Doc depth",
918			facetOpts: []FacetSearchOption{
919				FacetDocumentDepth(123),
920			},
921			want: &pb.SearchParams{
922				FacetDepth: proto.Int32(123),
923			},
924		},
925		{
926			desc: "Facet discovery",
927			facetOpts: []FacetSearchOption{
928				FacetDiscovery("colour"),
929				FacetDiscovery("size", Atom("M"), Atom("L")),
930				FacetDiscovery("price", LessThan(7), Range{7, 14}, AtLeast(14)),
931			},
932			want: &pb.SearchParams{
933				IncludeFacet: []*pb.FacetRequest{
934					{Name: proto.String("colour")},
935					{Name: proto.String("size"), Params: &pb.FacetRequestParam{
936						ValueConstraint: []string{"M", "L"},
937					}},
938					{Name: proto.String("price"), Params: &pb.FacetRequestParam{
939						Range: []*pb.FacetRange{
940							{End: proto.String("7e+00")},
941							{Start: proto.String("7e+00"), End: proto.String("1.4e+01")},
942							{Start: proto.String("1.4e+01")},
943						},
944					}},
945				},
946			},
947		},
948		{
949			desc: "Facet discovery - bad value",
950			facetOpts: []FacetSearchOption{
951				FacetDiscovery("colour", true),
952			},
953			wantErr: "bad FacetSearchOption: unsupported value type bool",
954		},
955		{
956			desc: "Facet discovery - mix value types",
957			facetOpts: []FacetSearchOption{
958				FacetDiscovery("colour", Atom("blue"), AtLeast(7)),
959			},
960			wantErr: "bad FacetSearchOption: values must all be Atom, or must all be Range",
961		},
962		{
963			desc: "Facet discovery - invalid range",
964			facetOpts: []FacetSearchOption{
965				FacetDiscovery("colour", Range{negInf, posInf}),
966			},
967			wantErr: "bad FacetSearchOption: invalid range: either Start or End must be finite",
968		},
969		{
970			desc:   "Cursor",
971			cursor: Cursor("mycursor"),
972			want: &pb.SearchParams{
973				Cursor: proto.String("mycursor"),
974			},
975		},
976		{
977			desc:   "Offset",
978			offset: 121,
979			want: &pb.SearchParams{
980				Offset: proto.Int32(121),
981			},
982		},
983		{
984			desc:    "Cursor and Offset set",
985			cursor:  Cursor("mycursor"),
986			offset:  121,
987			wantErr: "at most one of Cursor and Offset may be specified",
988		},
989		{
990			desc:          "Count accuracy",
991			countAccuracy: 100,
992			want: &pb.SearchParams{
993				MatchedCountAccuracy: proto.Int32(100),
994			},
995		},
996	}
997
998	for _, tt := range testCases {
999		c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
1000			if tt.want == nil {
1001				t.Errorf("%s: expected call to fail", tt.desc)
1002				return nil
1003			}
1004			// Set default fields.
1005			tt.want.Query = proto.String("gopher")
1006			tt.want.IndexSpec = &pb.IndexSpec{Name: proto.String("Doc")}
1007			tt.want.CursorType = pb.SearchParams_PER_RESULT.Enum()
1008			tt.want.FieldSpec = &pb.FieldSpec{}
1009			if got := req.Params; !reflect.DeepEqual(got, tt.want) {
1010				t.Errorf("%s: params=%v; want %v", tt.desc, got, tt.want)
1011			}
1012			return noErr // Always return some error to prevent response parsing.
1013		})
1014
1015		it := index.Search(c, "gopher", &SearchOptions{
1016			Facets:        tt.facetOpts,
1017			Cursor:        tt.cursor,
1018			Offset:        tt.offset,
1019			CountAccuracy: tt.countAccuracy,
1020		})
1021		_, err := it.Next(nil)
1022		if err == nil {
1023			t.Fatalf("%s: err==nil; should not happen", tt.desc)
1024		}
1025		if err.Error() != tt.wantErr {
1026			t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
1027		}
1028	}
1029}
1030
1031func TestFacetRefinements(t *testing.T) {
1032	index, err := Open("Doc")
1033	if err != nil {
1034		t.Fatalf("err from Open: %v", err)
1035	}
1036
1037	noErr := errors.New("") // Sentinel err to return to prevent sending request.
1038
1039	testCases := []struct {
1040		desc    string
1041		refine  []Facet
1042		want    []*pb.FacetRefinement
1043		wantErr string
1044	}{
1045		{
1046			desc: "No refinements",
1047		},
1048		{
1049			desc: "Basic",
1050			refine: []Facet{
1051				{Name: "fur", Value: Atom("fluffy")},
1052				{Name: "age", Value: LessThan(123)},
1053				{Name: "age", Value: AtLeast(0)},
1054				{Name: "legs", Value: Range{Start: 3, End: 5}},
1055			},
1056			want: []*pb.FacetRefinement{
1057				{Name: proto.String("fur"), Value: proto.String("fluffy")},
1058				{Name: proto.String("age"), Range: &pb.FacetRefinement_Range{End: proto.String("1.23e+02")}},
1059				{Name: proto.String("age"), Range: &pb.FacetRefinement_Range{Start: proto.String("0e+00")}},
1060				{Name: proto.String("legs"), Range: &pb.FacetRefinement_Range{Start: proto.String("3e+00"), End: proto.String("5e+00")}},
1061			},
1062		},
1063		{
1064			desc: "Infinite range",
1065			refine: []Facet{
1066				{Name: "age", Value: Range{Start: negInf, End: posInf}},
1067			},
1068			wantErr: `search: refinement for facet "age": either Start or End must be finite`,
1069		},
1070		{
1071			desc: "Bad End value in range",
1072			refine: []Facet{
1073				{Name: "age", Value: LessThan(2147483648)},
1074			},
1075			wantErr: `search: refinement for facet "age": invalid value for End`,
1076		},
1077		{
1078			desc: "Bad Start value in range",
1079			refine: []Facet{
1080				{Name: "age", Value: AtLeast(-2147483649)},
1081			},
1082			wantErr: `search: refinement for facet "age": invalid value for Start`,
1083		},
1084		{
1085			desc: "Unknown value type",
1086			refine: []Facet{
1087				{Name: "age", Value: "you can't use strings!"},
1088			},
1089			wantErr: `search: unsupported refinement for facet "age" of type string`,
1090		},
1091	}
1092
1093	for _, tt := range testCases {
1094		c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
1095			if got := req.Params.FacetRefinement; !reflect.DeepEqual(got, tt.want) {
1096				t.Errorf("%s: params.FacetRefinement=%v; want %v", tt.desc, got, tt.want)
1097			}
1098			return noErr // Always return some error to prevent response parsing.
1099		})
1100
1101		it := index.Search(c, "gopher", &SearchOptions{Refinements: tt.refine})
1102		_, err := it.Next(nil)
1103		if err == nil {
1104			t.Fatalf("%s: err==nil; should not happen", tt.desc)
1105		}
1106		if err.Error() != tt.wantErr {
1107			t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
1108		}
1109	}
1110}
1111
1112func TestNamespaceResetting(t *testing.T) {
1113	namec := make(chan *string, 1)
1114	c0 := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(req *pb.IndexDocumentRequest, res *pb.IndexDocumentResponse) error {
1115		namec <- req.Params.IndexSpec.Namespace
1116		return fmt.Errorf("RPC error")
1117	})
1118
1119	// Check that wrapping c0 in a namespace twice works correctly.
1120	c1, err := appengine.Namespace(c0, "A")
1121	if err != nil {
1122		t.Fatalf("appengine.Namespace: %v", err)
1123	}
1124	c2, err := appengine.Namespace(c1, "") // should act as the original context
1125	if err != nil {
1126		t.Fatalf("appengine.Namespace: %v", err)
1127	}
1128
1129	i := (&Index{})
1130
1131	i.Put(c0, "something", &searchDoc)
1132	if ns := <-namec; ns != nil {
1133		t.Errorf(`Put with c0: ns = %q, want nil`, *ns)
1134	}
1135
1136	i.Put(c1, "something", &searchDoc)
1137	if ns := <-namec; ns == nil {
1138		t.Error(`Put with c1: ns = nil, want "A"`)
1139	} else if *ns != "A" {
1140		t.Errorf(`Put with c1: ns = %q, want "A"`, *ns)
1141	}
1142
1143	i.Put(c2, "something", &searchDoc)
1144	if ns := <-namec; ns != nil {
1145		t.Errorf(`Put with c2: ns = %q, want nil`, *ns)
1146	}
1147}
1148
1149func TestDelete(t *testing.T) {
1150	index, err := Open("Doc")
1151	if err != nil {
1152		t.Fatalf("err from Open: %v", err)
1153	}
1154
1155	c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error {
1156		expectedIn := &pb.DeleteDocumentRequest{
1157			Params: &pb.DeleteDocumentParams{
1158				DocId:     []string{"id"},
1159				IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")},
1160			},
1161		}
1162		if !proto.Equal(in, expectedIn) {
1163			return fmt.Errorf("unsupported argument:\ngot  %v\nwant %v", in, expectedIn)
1164		}
1165		*out = pb.DeleteDocumentResponse{
1166			Status: []*pb.RequestStatus{
1167				{Code: pb.SearchServiceError_OK.Enum()},
1168			},
1169		}
1170		return nil
1171	})
1172
1173	if err := index.Delete(c, "id"); err != nil {
1174		t.Fatal(err)
1175	}
1176}
1177
1178func TestDeleteMulti(t *testing.T) {
1179	index, err := Open("Doc")
1180	if err != nil {
1181		t.Fatalf("err from Open: %v", err)
1182	}
1183
1184	c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error {
1185		expectedIn := &pb.DeleteDocumentRequest{
1186			Params: &pb.DeleteDocumentParams{
1187				DocId:     []string{"id1", "id2"},
1188				IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")},
1189			},
1190		}
1191		if !proto.Equal(in, expectedIn) {
1192			return fmt.Errorf("unsupported argument:\ngot  %v\nwant %v", in, expectedIn)
1193		}
1194		*out = pb.DeleteDocumentResponse{
1195			Status: []*pb.RequestStatus{
1196				{Code: pb.SearchServiceError_OK.Enum()},
1197				{Code: pb.SearchServiceError_OK.Enum()},
1198			},
1199		}
1200		return nil
1201	})
1202
1203	if err := index.DeleteMulti(c, []string{"id1", "id2"}); err != nil {
1204		t.Fatal(err)
1205	}
1206}
1207
1208func TestDeleteWrongNumberOfResults(t *testing.T) {
1209	index, err := Open("Doc")
1210	if err != nil {
1211		t.Fatalf("err from Open: %v", err)
1212	}
1213
1214	c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error {
1215		expectedIn := &pb.DeleteDocumentRequest{
1216			Params: &pb.DeleteDocumentParams{
1217				DocId:     []string{"id1", "id2"},
1218				IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")},
1219			},
1220		}
1221		if !proto.Equal(in, expectedIn) {
1222			return fmt.Errorf("unsupported argument:\ngot  %v\nwant %v", in, expectedIn)
1223		}
1224		*out = pb.DeleteDocumentResponse{
1225			Status: []*pb.RequestStatus{
1226				{Code: pb.SearchServiceError_OK.Enum()},
1227			},
1228		}
1229		return nil
1230	})
1231
1232	if err := index.DeleteMulti(c, []string{"id1", "id2"}); err == nil {
1233		t.Fatalf("got nil, want error")
1234	}
1235}
1236
1237func TestDeleteMultiError(t *testing.T) {
1238	index, err := Open("Doc")
1239	if err != nil {
1240		t.Fatalf("err from Open: %v", err)
1241	}
1242
1243	c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error {
1244		expectedIn := &pb.DeleteDocumentRequest{
1245			Params: &pb.DeleteDocumentParams{
1246				DocId:     []string{"id1", "id2"},
1247				IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")},
1248			},
1249		}
1250		if !proto.Equal(in, expectedIn) {
1251			return fmt.Errorf("unsupported argument:\ngot  %v\nwant %v", in, expectedIn)
1252		}
1253		*out = pb.DeleteDocumentResponse{
1254			Status: []*pb.RequestStatus{
1255				{Code: pb.SearchServiceError_OK.Enum()},
1256				{Code: pb.SearchServiceError_PERMISSION_DENIED.Enum(), ErrorDetail: proto.String("foo")},
1257			},
1258		}
1259		return nil
1260	})
1261
1262	switch err := index.DeleteMulti(c, []string{"id1", "id2"}); {
1263	case err == nil:
1264		t.Fatalf("got nil, want error")
1265	case err.(appengine.MultiError)[0] != nil:
1266		t.Fatalf("got %v, want nil MultiError[0]", err.(appengine.MultiError)[0])
1267	case err.(appengine.MultiError)[1] == nil:
1268		t.Fatalf("got nil, want not-nill MultiError[1]")
1269	}
1270}
1271