github.com/Jeffail/benthos/v3@v3.65.0/internal/impl/confluent/processor_schema_registry_decode_test.go (about) 1 package confluent 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/http/httptest" 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 TestSchemaRegistryDecoderConfigParse(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 `, 29 errContains: `failed to parse url`, 30 }, 31 { 32 name: "url with base path", 33 config: ` 34 url: http://example.com/v1 35 `, 36 expectedBaseURL: "http://example.com/v1", 37 }, 38 } 39 40 spec := schemaRegistryDecoderConfig() 41 env := service.NewEnvironment() 42 for _, test := range configTests { 43 t.Run(test.name, func(t *testing.T) { 44 conf, err := spec.ParseYAML(test.config, env) 45 require.NoError(t, err) 46 47 e, err := newSchemaRegistryDecoderFromConfig(conf, nil) 48 49 if e != nil { 50 assert.Equal(t, test.expectedBaseURL, e.schemaRegistryBaseURL.String()) 51 } 52 53 if err == nil { 54 _ = e.Close(context.Background()) 55 } 56 if test.errContains == "" { 57 require.NoError(t, err) 58 } else { 59 require.Error(t, err) 60 assert.Contains(t, err.Error(), test.errContains) 61 } 62 }) 63 } 64 } 65 66 func runSchemaRegistryServer(t *testing.T, fn func(path string) ([]byte, error)) string { 67 t.Helper() 68 69 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 b, err := fn(r.URL.Path) 71 if err != nil { 72 http.Error(w, err.Error(), http.StatusBadRequest) 73 return 74 } 75 if len(b) == 0 { 76 http.Error(w, "not found", http.StatusNotFound) 77 return 78 } 79 w.Write(b) 80 })) 81 t.Cleanup(ts.Close) 82 83 return ts.URL 84 } 85 86 const testSchema = `{ 87 "namespace": "foo.namespace.com", 88 "type": "record", 89 "name": "identity", 90 "fields": [ 91 { "name": "Name", "type": "string"}, 92 { "name": "Address", "type": ["null",{ 93 "namespace": "my.namespace.com", 94 "type": "record", 95 "name": "address", 96 "fields": [ 97 { "name": "City", "type": "string" }, 98 { "name": "State", "type": "string" } 99 ] 100 }],"default":null}, 101 {"name": "MaybeHobby", "type": ["null","string"] } 102 ] 103 }` 104 105 func TestSchemaRegistryDecodeAvro(t *testing.T) { 106 payload3, err := json.Marshal(struct { 107 Schema string `json:"schema"` 108 }{ 109 Schema: testSchema, 110 }) 111 require.NoError(t, err) 112 113 returnedSchema3 := false 114 urlStr := runSchemaRegistryServer(t, func(path string) ([]byte, error) { 115 switch path { 116 case "/schemas/ids/3": 117 assert.False(t, returnedSchema3) 118 returnedSchema3 = true 119 return payload3, nil 120 case "/schemas/ids/5": 121 return nil, fmt.Errorf("nope") 122 } 123 return nil, nil 124 }) 125 126 decoder, err := newSchemaRegistryDecoder(urlStr, nil, false, nil) 127 require.NoError(t, err) 128 129 tests := []struct { 130 name string 131 input string 132 output string 133 errContains string 134 }{ 135 { 136 name: "successful message", 137 input: "\x00\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar\x02\x0edancing", 138 output: `{"Address":{"my.namespace.com.address":{"City":"foo","State":"bar"}},"MaybeHobby":{"string":"dancing"},"Name":"foo"}`, 139 }, 140 { 141 name: "successful message with null hobby", 142 input: "\x00\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar\x00", 143 output: `{"Address":{"my.namespace.com.address":{"City":"foo","State":"bar"}},"MaybeHobby":null,"Name":"foo"}`, 144 }, 145 { 146 name: "non-empty magic byte", 147 input: "\x06\x00\x00\x00\x03\x06foo\x02\x06foo\x06bar", 148 errContains: "version number 6 not supported", 149 }, 150 { 151 name: "non-existing schema", 152 input: "\x00\x00\x00\x00\x04\x06foo\x02\x06foo\x06bar", 153 errContains: "schema '4' not found by registry", 154 }, 155 { 156 name: "server fails", 157 input: "\x00\x00\x00\x00\x05\x06foo\x02\x06foo\x06bar", 158 errContains: "request failed for schema '5'", 159 }, 160 } 161 162 for _, test := range tests { 163 test := test 164 t.Run(test.name, func(t *testing.T) { 165 outMsgs, err := decoder.Process(context.Background(), service.NewMessage([]byte(test.input))) 166 if test.errContains != "" { 167 require.Error(t, err) 168 assert.Contains(t, err.Error(), test.errContains) 169 } else { 170 require.NoError(t, err) 171 require.Len(t, outMsgs, 1) 172 173 b, err := outMsgs[0].AsBytes() 174 require.NoError(t, err) 175 assert.Equal(t, test.output, string(b)) 176 } 177 178 }) 179 } 180 181 require.NoError(t, decoder.Close(context.Background())) 182 decoder.cacheMut.Lock() 183 assert.Len(t, decoder.schemas, 0) 184 decoder.cacheMut.Unlock() 185 } 186 187 func TestSchemaRegistryDecodeClearExpired(t *testing.T) { 188 urlStr := runSchemaRegistryServer(t, func(path string) ([]byte, error) { 189 return nil, fmt.Errorf("nope") 190 }) 191 192 decoder, err := newSchemaRegistryDecoder(urlStr, nil, false, nil) 193 require.NoError(t, err) 194 require.NoError(t, decoder.Close(context.Background())) 195 196 tStale := time.Now().Add(-time.Hour).Unix() 197 tNotStale := time.Now().Unix() 198 tNearlyStale := time.Now().Add(-(schemaStaleAfter / 2)).Unix() 199 200 decoder.cacheMut.Lock() 201 decoder.schemas = map[int]*cachedSchemaDecoder{ 202 5: {lastUsedUnixSeconds: tStale}, 203 10: {lastUsedUnixSeconds: tNotStale}, 204 15: {lastUsedUnixSeconds: tNearlyStale}, 205 } 206 decoder.cacheMut.Unlock() 207 208 decoder.clearExpired() 209 210 decoder.cacheMut.Lock() 211 assert.Equal(t, map[int]*cachedSchemaDecoder{ 212 10: {lastUsedUnixSeconds: tNotStale}, 213 15: {lastUsedUnixSeconds: tNearlyStale}, 214 }, decoder.schemas) 215 decoder.cacheMut.Unlock() 216 }