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  }