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 }