go.temporal.io/server@v1.23.0/common/searchattribute/stringify_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package searchattribute
    26  
    27  import (
    28  	"errors"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/stretchr/testify/suite"
    33  	commonpb "go.temporal.io/api/common/v1"
    34  	enumspb "go.temporal.io/api/enums/v1"
    35  )
    36  
    37  type StringifySuite struct {
    38  	suite.Suite
    39  }
    40  
    41  func TestStringifySuite(t *testing.T) {
    42  	s := &StringifySuite{}
    43  	suite.Run(t, s)
    44  }
    45  
    46  func (s *StringifySuite) SetupSuite() {
    47  }
    48  
    49  func (s *StringifySuite) SetupTest() {
    50  }
    51  
    52  func (s *StringifySuite) TearDownTest() {
    53  }
    54  
    55  func (s *StringifySuite) Test_Stringify() {
    56  	typeMap := NameTypeMap{
    57  		customSearchAttributes: map[string]enumspb.IndexedValueType{
    58  			"key1": enumspb.INDEXED_VALUE_TYPE_TEXT,
    59  			"key2": enumspb.INDEXED_VALUE_TYPE_INT,
    60  			"key3": enumspb.INDEXED_VALUE_TYPE_BOOL,
    61  		},
    62  	}
    63  
    64  	sa, err := Encode(map[string]interface{}{
    65  		"key1": "val1",
    66  		"key2": 2,
    67  		"key3": true,
    68  	}, &typeMap)
    69  	s.NoError(err)
    70  
    71  	saStr, err := Stringify(sa, nil)
    72  	s.NoError(err)
    73  	s.Len(saStr, 3)
    74  	s.Equal("val1", saStr["key1"])
    75  	s.Equal("2", saStr["key2"])
    76  	s.Equal("true", saStr["key3"])
    77  
    78  	// Clean Metadata type and use typeMap.
    79  	delete(sa.IndexedFields["key1"].Metadata, "type")
    80  	delete(sa.IndexedFields["key2"].Metadata, "type")
    81  	delete(sa.IndexedFields["key3"].Metadata, "type")
    82  
    83  	saStr, err = Stringify(sa, &typeMap)
    84  	s.NoError(err)
    85  	s.Len(saStr, 3)
    86  	s.Equal("val1", saStr["key1"])
    87  	s.Equal("2", saStr["key2"])
    88  	s.Equal("true", saStr["key3"])
    89  
    90  	// Even w/o typeMap error is returned but string values are set with  raw JSON from GetData().
    91  	saStr, err = Stringify(sa, nil)
    92  	s.Error(err)
    93  	s.True(errors.Is(err, ErrInvalidType))
    94  	s.Len(saStr, 3)
    95  	s.Equal(`"val1"`, saStr["key1"])
    96  	s.Equal("2", saStr["key2"])
    97  	s.Equal("true", saStr["key3"])
    98  }
    99  
   100  func (s *StringifySuite) Test_Stringify_Array() {
   101  	typeMap := NameTypeMap{
   102  		customSearchAttributes: map[string]enumspb.IndexedValueType{
   103  			"key1": enumspb.INDEXED_VALUE_TYPE_TEXT,
   104  			"key2": enumspb.INDEXED_VALUE_TYPE_INT,
   105  			"key3": enumspb.INDEXED_VALUE_TYPE_BOOL,
   106  		},
   107  	}
   108  
   109  	sa, err := Encode(map[string]interface{}{
   110  		"key1": []string{"val1", "val2"},
   111  		"key2": []int64{2, 3, 4},
   112  		"key3": []bool{true, false, true},
   113  	}, &typeMap)
   114  	s.NoError(err)
   115  
   116  	saStr, err := Stringify(sa, nil)
   117  	s.NoError(err)
   118  	s.Len(saStr, 3)
   119  	s.Equal(`["val1","val2"]`, saStr["key1"])
   120  	s.Equal("[2,3,4]", saStr["key2"])
   121  	s.Equal("[true,false,true]", saStr["key3"])
   122  
   123  	// Clean Metadata type and use typeMap.
   124  	delete(sa.IndexedFields["key1"].Metadata, "type")
   125  	delete(sa.IndexedFields["key2"].Metadata, "type")
   126  	delete(sa.IndexedFields["key3"].Metadata, "type")
   127  
   128  	saStr, err = Stringify(sa, &typeMap)
   129  	s.NoError(err)
   130  	s.Len(saStr, 3)
   131  	s.Equal(`["val1","val2"]`, saStr["key1"])
   132  	s.Equal("[2,3,4]", saStr["key2"])
   133  	s.Equal("[true,false,true]", saStr["key3"])
   134  
   135  	// Even w/o typeMap error is returned but string values are set with  raw JSON from GetData().
   136  	saStr, err = Stringify(sa, nil)
   137  	s.Error(err)
   138  	s.True(errors.Is(err, ErrInvalidType))
   139  	s.Len(saStr, 3)
   140  	s.Equal(`["val1","val2"]`, saStr["key1"])
   141  	s.Equal("[2,3,4]", saStr["key2"])
   142  	s.Equal("[true,false,true]", saStr["key3"])
   143  }
   144  
   145  func (s *StringifySuite) Test_Parse_ValidTypeMap() {
   146  	sa, err := Parse(map[string]string{
   147  		"key1": "val1",
   148  		"key2": "2",
   149  		"key3": "true",
   150  	}, &NameTypeMap{
   151  		customSearchAttributes: map[string]enumspb.IndexedValueType{
   152  			"key1": enumspb.INDEXED_VALUE_TYPE_TEXT,
   153  			"key2": enumspb.INDEXED_VALUE_TYPE_INT,
   154  			"key3": enumspb.INDEXED_VALUE_TYPE_BOOL,
   155  		}})
   156  
   157  	s.NoError(err)
   158  	s.Len(sa.IndexedFields, 3)
   159  	s.Equal(`"val1"`, string(sa.IndexedFields["key1"].GetData()))
   160  	s.Equal("Text", string(sa.IndexedFields["key1"].GetMetadata()["type"]))
   161  	s.Equal("2", string(sa.IndexedFields["key2"].GetData()))
   162  	s.Equal("Int", string(sa.IndexedFields["key2"].GetMetadata()["type"]))
   163  	s.Equal("true", string(sa.IndexedFields["key3"].GetData()))
   164  	s.Equal("Bool", string(sa.IndexedFields["key3"].GetMetadata()["type"]))
   165  }
   166  
   167  func (s *StringifySuite) Test_Parse_NilTypeMap() {
   168  	sa, err := Parse(map[string]string{
   169  		"key1": "val1",
   170  		"key2": "2",
   171  		"key3": "true",
   172  	}, nil)
   173  
   174  	s.NoError(err)
   175  	s.Len(sa.IndexedFields, 3)
   176  	s.Equal(`"val1"`, string(sa.IndexedFields["key1"].GetData()))
   177  	s.Equal("2", string(sa.IndexedFields["key2"].GetData()))
   178  	s.Equal("true", string(sa.IndexedFields["key3"].GetData()))
   179  
   180  }
   181  func (s *StringifySuite) Test_Parse_WrongTypesInTypeMap() {
   182  	sa, err := Parse(map[string]string{
   183  		"key1": "val1",
   184  		"key2": "2",
   185  	}, &NameTypeMap{
   186  		customSearchAttributes: map[string]enumspb.IndexedValueType{
   187  			"key1": enumspb.INDEXED_VALUE_TYPE_INT,
   188  			"key2": enumspb.INDEXED_VALUE_TYPE_TEXT,
   189  		}})
   190  
   191  	s.Error(err)
   192  	s.Len(sa.IndexedFields, 2)
   193  	s.Nil(sa.IndexedFields["key1"])
   194  	s.Equal(`"2"`, string(sa.IndexedFields["key2"].GetData()))
   195  	s.Equal("Text", string(sa.IndexedFields["key2"].GetMetadata()["type"]))
   196  }
   197  
   198  func (s *StringifySuite) Test_Parse_MissedFieldsInTypeMap() {
   199  	sa, err := Parse(map[string]string{
   200  		"key1": "val1",
   201  		"key2": "2",
   202  	}, &NameTypeMap{
   203  		customSearchAttributes: map[string]enumspb.IndexedValueType{
   204  			"key3": enumspb.INDEXED_VALUE_TYPE_TEXT,
   205  			"key2": enumspb.INDEXED_VALUE_TYPE_INT,
   206  		}})
   207  
   208  	s.NoError(err)
   209  	s.Len(sa.IndexedFields, 2)
   210  	s.Equal(`"val1"`, string(sa.IndexedFields["key1"].GetData()))
   211  	s.Nil(sa.IndexedFields["key1"].GetMetadata()["type"])
   212  	s.Equal("2", string(sa.IndexedFields["key2"].GetData()))
   213  	s.Equal("Int", string(sa.IndexedFields["key2"].GetMetadata()["type"]))
   214  }
   215  
   216  func (s *StringifySuite) Test_Parse_Array() {
   217  	sa, err := Parse(map[string]string{
   218  		"key1": ` ["val1", "val2"] `,
   219  		"key2": "[2,3,4]",
   220  	}, &NameTypeMap{
   221  		customSearchAttributes: map[string]enumspb.IndexedValueType{
   222  			"key1": enumspb.INDEXED_VALUE_TYPE_TEXT,
   223  			"key2": enumspb.INDEXED_VALUE_TYPE_INT,
   224  		}})
   225  
   226  	s.NoError(err)
   227  	s.Len(sa.IndexedFields, 2)
   228  	s.Equal(`["val1","val2"]`, string(sa.IndexedFields["key1"].GetData()))
   229  	s.Equal("Text", string(sa.IndexedFields["key1"].GetMetadata()["type"]))
   230  	s.Equal("[2,3,4]", string(sa.IndexedFields["key2"].GetData()))
   231  	s.Equal("Int", string(sa.IndexedFields["key2"].GetMetadata()["type"]))
   232  }
   233  
   234  func (s *StringifySuite) Test_parseValueOrArray() {
   235  	var res *commonpb.Payload
   236  	var err error
   237  
   238  	// int
   239  	res, err = parseValueOrArray("1", enumspb.INDEXED_VALUE_TYPE_INT)
   240  	s.NoError(err)
   241  	s.Equal("Int", string(res.Metadata["type"]))
   242  	s.Equal("1", string(res.Data))
   243  
   244  	// array must be in JSON format.
   245  	res, err = parseValueOrArray(`["qwe"]`, enumspb.INDEXED_VALUE_TYPE_TEXT)
   246  	s.NoError(err)
   247  	s.Equal("Text", string(res.Metadata["type"]))
   248  	s.Equal(`["qwe"]`, string(res.Data))
   249  
   250  	// array must be in JSON format.
   251  	res, err = parseValueOrArray(`[qwe]`, enumspb.INDEXED_VALUE_TYPE_TEXT)
   252  	s.Error(err)
   253  	s.Nil(res)
   254  }
   255  
   256  func (s *StringifySuite) Test_parseValueTyped() {
   257  	var res interface{}
   258  	var err error
   259  
   260  	// int
   261  	res, err = parseValueTyped("1", enumspb.INDEXED_VALUE_TYPE_INT)
   262  	s.NoError(err)
   263  	s.Equal(int64(1), res)
   264  
   265  	res, err = parseValueTyped("qwe", enumspb.INDEXED_VALUE_TYPE_INT)
   266  	s.Error(err)
   267  	s.Equal(int64(0), res)
   268  
   269  	// bool
   270  	res, err = parseValueTyped("true", enumspb.INDEXED_VALUE_TYPE_BOOL)
   271  	s.NoError(err)
   272  	s.Equal(true, res)
   273  	res, err = parseValueTyped("false", enumspb.INDEXED_VALUE_TYPE_BOOL)
   274  	s.NoError(err)
   275  	s.Equal(false, res)
   276  	res, err = parseValueTyped("qwe", enumspb.INDEXED_VALUE_TYPE_BOOL)
   277  	s.Error(err)
   278  	s.Equal(false, res)
   279  
   280  	// double
   281  	res, err = parseValueTyped("1.0", enumspb.INDEXED_VALUE_TYPE_DOUBLE)
   282  	s.NoError(err)
   283  	s.Equal(float64(1.0), res)
   284  
   285  	res, err = parseValueTyped("qwe", enumspb.INDEXED_VALUE_TYPE_DOUBLE)
   286  	s.Error(err)
   287  	s.Equal(float64(0), res)
   288  
   289  	// datetime
   290  	res, err = parseValueTyped("2019-01-01T01:01:01Z", enumspb.INDEXED_VALUE_TYPE_DATETIME)
   291  	s.NoError(err)
   292  	s.Equal(time.Date(2019, 1, 1, 1, 1, 1, 0, time.UTC), res)
   293  
   294  	res, err = parseValueTyped("qwe", enumspb.INDEXED_VALUE_TYPE_DATETIME)
   295  	s.Error(err)
   296  	s.Equal(time.Time{}, res)
   297  
   298  	// string
   299  	res, err = parseValueTyped("test string", enumspb.INDEXED_VALUE_TYPE_TEXT)
   300  	s.NoError(err)
   301  	s.Equal("test string", res)
   302  
   303  	// unspecified
   304  	res, err = parseValueTyped("test string", enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED)
   305  	s.NoError(err)
   306  	s.Equal("test string", res)
   307  }
   308  
   309  func (s *StringifySuite) Test_parseValueUnspecified() {
   310  	var res interface{}
   311  
   312  	// int
   313  	res = parseValueUnspecified("1")
   314  	s.Equal(int64(1), res)
   315  
   316  	// bool
   317  	res = parseValueUnspecified("true")
   318  	s.Equal(true, res)
   319  	res = parseValueUnspecified("false")
   320  	s.Equal(false, res)
   321  
   322  	// double
   323  	res = parseValueUnspecified("1.0")
   324  	s.Equal(float64(1.0), res)
   325  
   326  	// datetime
   327  	res = parseValueUnspecified("2019-01-01T01:01:01Z")
   328  	s.Equal(time.Date(2019, 1, 1, 1, 1, 1, 0, time.UTC), res)
   329  
   330  	// array
   331  	res = parseValueUnspecified(`["a", "b", "c"]`)
   332  	s.Equal([]interface{}{"a", "b", "c"}, res)
   333  
   334  	// string
   335  	res = parseValueUnspecified("test string")
   336  	s.Equal("test string", res)
   337  }
   338  
   339  func (s *StringifySuite) Test_isJsonArray() {
   340  	s.True(isJsonArray("[1,2,3]"))
   341  	s.True(isJsonArray("  [1,2,3] "))
   342  	s.True(isJsonArray(`  ["1","2","3"] `))
   343  	s.True(isJsonArray("[]"))
   344  	s.False(isJsonArray("["))
   345  	s.False(isJsonArray("]"))
   346  	s.False(isJsonArray("qwe"))
   347  	s.False(isJsonArray("123"))
   348  }
   349  
   350  func (s *StringifySuite) Test_parseJsonArray() {
   351  	t1, _ := time.Parse(time.RFC3339Nano, "2019-06-07T16:16:34-08:00")
   352  	t2, _ := time.Parse(time.RFC3339Nano, "2019-06-07T17:16:34-08:00")
   353  	testCases := []struct {
   354  		name             string
   355  		indexedValueType enumspb.IndexedValueType
   356  		input            string
   357  		expected         interface{}
   358  	}{
   359  		{
   360  			name:             "string",
   361  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_TEXT,
   362  			input:            `["a", "b", "c"]`,
   363  			expected:         []string{"a", "b", "c"},
   364  		},
   365  		{
   366  			name:             "int",
   367  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_INT,
   368  			input:            `[1, 2, 3]`,
   369  			expected:         []int64{1, 2, 3},
   370  		},
   371  		{
   372  			name:             "double",
   373  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_DOUBLE,
   374  			input:            `[1.1, 2.2, 3.3]`,
   375  			expected:         []float64{1.1, 2.2, 3.3},
   376  		},
   377  		{
   378  			name:             "bool",
   379  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_BOOL,
   380  			input:            `[true, false]`,
   381  			expected:         []bool{true, false},
   382  		},
   383  		{
   384  			name:             "datetime",
   385  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_DATETIME,
   386  			input:            `["2019-06-07T16:16:34-08:00", "2019-06-07T17:16:34-08:00"]`,
   387  			expected:         []time.Time{t1, t2},
   388  		},
   389  		{
   390  			name:             "unspecified",
   391  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_UNSPECIFIED,
   392  			input:            `["a", "b", "c"]`,
   393  			expected:         []interface{}{"a", "b", "c"},
   394  		},
   395  	}
   396  	for _, testCase := range testCases {
   397  		s.Run(testCase.name, func() {
   398  			res, err := parseJsonArray(testCase.input, testCase.indexedValueType)
   399  			s.NoError(err)
   400  			s.Equal(testCase.expected, res)
   401  		})
   402  	}
   403  
   404  	testCases2 := []struct {
   405  		name             string
   406  		indexedValueType enumspb.IndexedValueType
   407  		input            string
   408  		expected         error
   409  	}{
   410  		{
   411  			name:             "not array",
   412  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_TEXT,
   413  			input:            "normal string",
   414  		},
   415  		{
   416  			name:             "empty string",
   417  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_TEXT,
   418  			input:            "",
   419  		},
   420  		{
   421  			name:             "not json array",
   422  			indexedValueType: enumspb.INDEXED_VALUE_TYPE_TEXT,
   423  			input:            "[a, b, c]",
   424  		},
   425  	}
   426  	for _, testCase := range testCases2 {
   427  		res, err := parseJsonArray(testCase.input, testCase.indexedValueType)
   428  		s.NotNil(err)
   429  		s.Nil(res)
   430  	}
   431  }