github.com/Jeffail/benthos/v3@v3.65.0/internal/impl/confluent/processor_schema_registry_encode_test.go (about) 1 package confluent 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "sync/atomic" 9 "testing" 10 "time" 11 12 "github.com/Jeffail/benthos/v3/public/service" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 ) 16 17 func TestSchemaRegistryEncoderConfigParse(t *testing.T) { 18 configTests := []struct { 19 name string 20 config string 21 errContains string 22 expectedBaseURL string 23 }{ 24 { 25 name: "bad url", 26 config: ` 27 url: huh#%#@$u*not////::example.com 28 subject: foo 29 `, 30 errContains: `failed to parse url`, 31 }, 32 { 33 name: "bad subject", 34 config: ` 35 url: http://example.com 36 subject: ${! bad interpolation } 37 `, 38 errContains: `failed to parse interpolated field`, 39 }, 40 { 41 name: "use default period", 42 config: ` 43 url: http://example.com 44 subject: foo 45 `, 46 expectedBaseURL: "http://example.com", 47 }, 48 { 49 name: "bad period", 50 config: ` 51 url: http://example.com 52 subject: foo 53 refresh_period: not a duration 54 `, 55 errContains: "invalid duration", 56 }, 57 { 58 name: "url with base path", 59 config: ` 60 url: http://example.com/v1 61 subject: foo 62 `, 63 expectedBaseURL: "http://example.com/v1", 64 }, 65 } 66 67 spec := schemaRegistryEncoderConfig() 68 env := service.NewEnvironment() 69 for _, test := range configTests { 70 t.Run(test.name, func(t *testing.T) { 71 conf, err := spec.ParseYAML(test.config, env) 72 require.NoError(t, err) 73 74 e, err := newSchemaRegistryEncoderFromConfig(conf, nil) 75 76 if e != nil { 77 assert.Equal(t, test.expectedBaseURL, e.schemaRegistryBaseURL.String()) 78 } 79 80 if err == nil { 81 _ = e.Close(context.Background()) 82 } 83 if test.errContains == "" { 84 require.NoError(t, err) 85 } else { 86 require.Error(t, err) 87 assert.Contains(t, err.Error(), test.errContains) 88 } 89 }) 90 } 91 } 92 93 func TestSchemaRegistryEncodeAvroRawJSON(t *testing.T) { 94 fooFirst, err := json.Marshal(struct { 95 Schema string `json:"schema"` 96 ID int `json:"id"` 97 }{ 98 Schema: testSchema, 99 ID: 3, 100 }) 101 require.NoError(t, err) 102 103 urlStr := runSchemaRegistryServer(t, func(path string) ([]byte, error) { 104 if path == "/subjects/foo/versions/latest" { 105 return fooFirst, nil 106 } 107 return nil, errors.New("nope") 108 }) 109 110 subj, err := service.NewInterpolatedString("foo") 111 require.NoError(t, err) 112 113 encoder, err := newSchemaRegistryEncoder(urlStr, nil, subj, true, time.Minute*10, time.Minute, nil) 114 require.NoError(t, err) 115 116 tests := []struct { 117 name string 118 input string 119 output string 120 errContains string 121 }{ 122 { 123 name: "successful message", 124 input: `{"Address":{"City":"foo","State":"bar"},"Name":"foo","MaybeHobby":"dancing"}`, 125 output: "\x00\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar\x02\x0edancing", 126 }, 127 { 128 name: "successful message null hobby", 129 input: `{"Address":{"City":"foo","State":"bar"},"Name":"foo","MaybeHobby":null}`, 130 output: "\x00\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar\x00", 131 }, 132 { 133 name: "message doesnt match schema", 134 input: `{"Address":{"City":"foo","State":30},"Name":"foo","MaybeHobby":null}`, 135 errContains: "could not decode any json data in input", 136 }, 137 } 138 139 for _, test := range tests { 140 test := test 141 t.Run(test.name, func(t *testing.T) { 142 outBatches, err := encoder.ProcessBatch( 143 context.Background(), 144 service.MessageBatch{service.NewMessage([]byte(test.input))}, 145 ) 146 require.NoError(t, err) 147 require.Len(t, outBatches, 1) 148 require.Len(t, outBatches[0], 1) 149 150 err = outBatches[0][0].GetError() 151 if test.errContains != "" { 152 require.Error(t, err) 153 assert.Contains(t, err.Error(), test.errContains) 154 } else { 155 require.NoError(t, err) 156 157 b, err := outBatches[0][0].AsBytes() 158 require.NoError(t, err) 159 assert.Equal(t, test.output, string(b)) 160 } 161 }) 162 } 163 164 require.NoError(t, encoder.Close(context.Background())) 165 encoder.cacheMut.Lock() 166 assert.Len(t, encoder.schemas, 0) 167 encoder.cacheMut.Unlock() 168 } 169 170 func TestSchemaRegistryEncodeAvro(t *testing.T) { 171 fooFirst, err := json.Marshal(struct { 172 Schema string `json:"schema"` 173 ID int `json:"id"` 174 }{ 175 Schema: testSchema, 176 ID: 3, 177 }) 178 require.NoError(t, err) 179 180 urlStr := runSchemaRegistryServer(t, func(path string) ([]byte, error) { 181 if path == "/subjects/foo/versions/latest" { 182 return fooFirst, nil 183 } 184 return nil, errors.New("nope") 185 }) 186 187 subj, err := service.NewInterpolatedString("foo") 188 require.NoError(t, err) 189 190 encoder, err := newSchemaRegistryEncoder(urlStr, nil, subj, false, time.Minute*10, time.Minute, nil) 191 require.NoError(t, err) 192 193 tests := []struct { 194 name string 195 input string 196 output string 197 errContains string 198 }{ 199 { 200 name: "successful message", 201 input: `{"Address":{"my.namespace.com.address":{"City":"foo","State":"bar"}},"Name":"foo","MaybeHobby":{"string":"dancing"}}`, 202 output: "\x00\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar\x02\x0edancing", 203 }, 204 { 205 name: "successful message null hobby", 206 input: `{"Address":{"my.namespace.com.address":{"City":"foo","State":"bar"}},"Name":"foo","MaybeHobby":null}`, 207 output: "\x00\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar\x00", 208 }, 209 { 210 name: "message doesnt match schema", 211 input: `{"Address":{"my.namespace.com.address":"not this","Name":"foo"}}`, 212 errContains: "schema does not specify default value", 213 }, 214 } 215 216 for _, test := range tests { 217 test := test 218 t.Run(test.name, func(t *testing.T) { 219 outBatches, err := encoder.ProcessBatch( 220 context.Background(), 221 service.MessageBatch{service.NewMessage([]byte(test.input))}, 222 ) 223 require.NoError(t, err) 224 require.Len(t, outBatches, 1) 225 require.Len(t, outBatches[0], 1) 226 227 err = outBatches[0][0].GetError() 228 if test.errContains != "" { 229 require.Error(t, err) 230 assert.Contains(t, err.Error(), test.errContains) 231 } else { 232 require.NoError(t, err) 233 234 b, err := outBatches[0][0].AsBytes() 235 require.NoError(t, err) 236 assert.Equal(t, test.output, string(b)) 237 } 238 }) 239 } 240 241 require.NoError(t, encoder.Close(context.Background())) 242 encoder.cacheMut.Lock() 243 assert.Len(t, encoder.schemas, 0) 244 encoder.cacheMut.Unlock() 245 } 246 247 func TestSchemaRegistryEncodeClearExpired(t *testing.T) { 248 urlStr := runSchemaRegistryServer(t, func(path string) ([]byte, error) { 249 return nil, fmt.Errorf("nope") 250 }) 251 252 subj, err := service.NewInterpolatedString("foo") 253 require.NoError(t, err) 254 255 encoder, err := newSchemaRegistryEncoder(urlStr, nil, subj, false, time.Minute*10, time.Minute, nil) 256 require.NoError(t, err) 257 require.NoError(t, encoder.Close(context.Background())) 258 259 tStale := time.Now().Add(-time.Hour).Unix() 260 tNotStale := time.Now().Unix() 261 tNearlyStale := time.Now().Add(-(schemaStaleAfter / 2)).Unix() 262 263 encoder.cacheMut.Lock() 264 encoder.schemas = map[string]*cachedSchemaEncoder{ 265 "5": {lastUsedUnixSeconds: tStale, lastUpdatedUnixSeconds: tNotStale}, 266 "10": {lastUsedUnixSeconds: tNotStale, lastUpdatedUnixSeconds: tNotStale}, 267 "15": {lastUsedUnixSeconds: tNearlyStale, lastUpdatedUnixSeconds: tNotStale}, 268 } 269 encoder.cacheMut.Unlock() 270 271 encoder.refreshEncoders() 272 273 encoder.cacheMut.Lock() 274 assert.Equal(t, map[string]*cachedSchemaEncoder{ 275 "10": {lastUsedUnixSeconds: tNotStale, lastUpdatedUnixSeconds: tNotStale}, 276 "15": {lastUsedUnixSeconds: tNearlyStale, lastUpdatedUnixSeconds: tNotStale}, 277 }, encoder.schemas) 278 encoder.cacheMut.Unlock() 279 } 280 281 func TestSchemaRegistryEncodeRefresh(t *testing.T) { 282 fooFirst, err := json.Marshal(struct { 283 Schema string `json:"schema"` 284 ID int `json:"id"` 285 }{ 286 Schema: testSchema, 287 ID: 2, 288 }) 289 require.NoError(t, err) 290 291 barFirst, err := json.Marshal(struct { 292 Schema string `json:"schema"` 293 ID int `json:"id"` 294 }{ 295 Schema: testSchema, 296 ID: 12, 297 }) 298 require.NoError(t, err) 299 300 var fooReqs, barReqs int32 301 urlStr := runSchemaRegistryServer(t, func(path string) ([]byte, error) { 302 switch path { 303 case "/subjects/foo/versions/latest": 304 atomic.AddInt32(&fooReqs, 1) 305 return fooFirst, nil 306 case "/subjects/bar/versions/latest": 307 atomic.AddInt32(&barReqs, 1) 308 return barFirst, nil 309 } 310 return nil, errors.New("nope") 311 }) 312 313 subj, err := service.NewInterpolatedString("foo") 314 require.NoError(t, err) 315 316 encoder, err := newSchemaRegistryEncoder(urlStr, nil, subj, false, time.Minute*10, time.Minute, nil) 317 require.NoError(t, err) 318 require.NoError(t, encoder.Close(context.Background())) 319 320 tStale := time.Now().Add(-time.Hour).Unix() 321 tNotStale := time.Now().Unix() 322 tNearlyStale := time.Now().Add(-(schemaStaleAfter / 2)).Unix() 323 324 encoder.nowFn = func() time.Time { 325 return time.Unix(tNotStale, 0) 326 } 327 328 encoder.cacheMut.Lock() 329 encoder.schemas = map[string]*cachedSchemaEncoder{ 330 "foo": { 331 lastUsedUnixSeconds: tNotStale, 332 lastUpdatedUnixSeconds: tStale, 333 id: 1, 334 }, 335 "bar": { 336 lastUsedUnixSeconds: tNotStale, 337 lastUpdatedUnixSeconds: tNearlyStale, 338 id: 11, 339 }, 340 } 341 encoder.cacheMut.Unlock() 342 343 assert.Equal(t, int32(0), atomic.LoadInt32(&fooReqs)) 344 assert.Equal(t, int32(0), atomic.LoadInt32(&barReqs)) 345 346 encoder.refreshEncoders() 347 348 encoder.cacheMut.Lock() 349 encoder.schemas["foo"].encoder = nil 350 assert.Equal(t, map[string]*cachedSchemaEncoder{ 351 "foo": { 352 lastUsedUnixSeconds: tNotStale, 353 lastUpdatedUnixSeconds: tNotStale, 354 id: 2, 355 }, 356 "bar": { 357 lastUsedUnixSeconds: tNotStale, 358 lastUpdatedUnixSeconds: tNearlyStale, 359 id: 11, 360 }, 361 }, encoder.schemas) 362 encoder.schemas["bar"].lastUpdatedUnixSeconds = tStale 363 encoder.cacheMut.Unlock() 364 365 assert.Equal(t, int32(1), atomic.LoadInt32(&fooReqs)) 366 assert.Equal(t, int32(0), atomic.LoadInt32(&barReqs)) 367 368 encoder.refreshEncoders() 369 370 encoder.cacheMut.Lock() 371 encoder.schemas["bar"].encoder = nil 372 assert.Equal(t, map[string]*cachedSchemaEncoder{ 373 "foo": { 374 lastUsedUnixSeconds: tNotStale, 375 lastUpdatedUnixSeconds: tNotStale, 376 id: 2, 377 }, 378 "bar": { 379 lastUsedUnixSeconds: tNotStale, 380 lastUpdatedUnixSeconds: tNotStale, 381 id: 12, 382 }, 383 }, encoder.schemas) 384 encoder.cacheMut.Unlock() 385 386 assert.Equal(t, int32(1), atomic.LoadInt32(&fooReqs)) 387 assert.Equal(t, int32(1), atomic.LoadInt32(&barReqs)) 388 }