avro_turf 0.7.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,28 @@
1
1
  require 'webmock/rspec'
2
2
  require 'avro_turf/messaging'
3
- require 'avro_turf/test/fake_schema_registry_server'
3
+ require 'avro_turf/test/fake_confluent_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::Messaging do
6
6
  let(:registry_url) { "http://registry.example.com" }
7
+ let(:client_cert) { "test client cert" }
8
+ let(:client_key) { "test client key" }
9
+ let(:client_key_pass) { "test client key password" }
7
10
  let(:logger) { Logger.new(StringIO.new) }
8
11
 
9
12
  let(:avro) {
10
13
  AvroTurf::Messaging.new(
11
14
  registry_url: registry_url,
12
15
  schemas_path: "spec/schemas",
13
- logger: logger
16
+ logger: logger,
17
+ client_cert: client_cert,
18
+ client_key: client_key,
19
+ client_key_pass: client_key_pass
14
20
  )
15
21
  }
16
22
 
17
23
  let(:message) { { "full_name" => "John Doe" } }
18
-
19
- before do
20
- FileUtils.mkdir_p("spec/schemas")
21
- end
22
-
23
- before do
24
- stub_request(:any, /^#{registry_url}/).to_rack(FakeSchemaRegistryServer)
25
- FakeSchemaRegistryServer.clear
26
- end
27
-
28
- before do
29
- define_schema "person.avsc", <<-AVSC
24
+ let(:schema_json) do
25
+ <<-AVSC
30
26
  {
31
27
  "name": "person",
32
28
  "type": "record",
@@ -39,8 +35,22 @@ describe AvroTurf::Messaging do
39
35
  }
40
36
  AVSC
41
37
  end
38
+ let(:schema) { Avro::Schema.parse(schema_json) }
39
+
40
+ before do
41
+ FileUtils.mkdir_p("spec/schemas")
42
+ end
43
+
44
+ before do
45
+ stub_request(:any, /^#{registry_url}/).to_rack(FakeConfluentSchemaRegistryServer)
46
+ FakeConfluentSchemaRegistryServer.clear
47
+ end
42
48
 
43
- shared_examples_for "encoding and decoding" do
49
+ before do
50
+ define_schema "person.avsc", schema_json
51
+ end
52
+
53
+ shared_examples_for "encoding and decoding with the schema from schema store" do
44
54
  it "encodes and decodes messages" do
45
55
  data = avro.encode(message, schema_name: "person")
46
56
  expect(avro.decode(data)).to eq message
@@ -60,10 +70,69 @@ describe AvroTurf::Messaging do
60
70
  end
61
71
  end
62
72
 
63
- it_behaves_like "encoding and decoding"
73
+ shared_examples_for 'encoding and decoding with the schema from registry' do
74
+ before do
75
+ registry = AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger)
76
+ registry.register('person', schema)
77
+ registry.register('people', schema)
78
+ end
79
+
80
+ it 'encodes and decodes messages' do
81
+ data = avro.encode(message, subject: 'person', version: 1)
82
+ expect(avro.decode(data)).to eq message
83
+ end
84
+
85
+ it "allows specifying a reader's schema by subject and version" do
86
+ data = avro.encode(message, subject: 'person', version: 1)
87
+ expect(avro.decode(data, schema_name: 'person')).to eq message
88
+ end
89
+
90
+ it 'raises AvroTurf::SchemaNotFoundError when the schema does not exist on registry' do
91
+ expect { avro.encode(message, subject: 'missing', version: 1) }.to raise_error(AvroTurf::SchemaNotFoundError)
92
+ end
93
+
94
+ it 'caches parsed schemas for decoding' do
95
+ data = avro.encode(message, subject: 'person', version: 1)
96
+ avro.decode(data)
97
+ allow(Avro::Schema).to receive(:parse).and_call_original
98
+ expect(avro.decode(data)).to eq message
99
+ expect(Avro::Schema).not_to have_received(:parse)
100
+ end
101
+ end
102
+
103
+ shared_examples_for 'encoding and decoding with the schema_id from registry' do
104
+ before do
105
+ registry = AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger)
106
+ registry.register('person', schema)
107
+ registry.register('people', schema)
108
+ end
109
+
110
+ it 'encodes and decodes messages' do
111
+ data = avro.encode(message, schema_id: 1)
112
+ expect(avro.decode(data)).to eq message
113
+ end
114
+
115
+ it 'raises AvroTurf::SchemaNotFoundError when the schema does not exist on registry' do
116
+ expect { avro.encode(message, schema_id: 5) }.to raise_error(AvroTurf::SchemaNotFoundError)
117
+ end
118
+
119
+ it 'caches parsed schemas for decoding' do
120
+ data = avro.encode(message, schema_id: 1)
121
+ avro.decode(data)
122
+ allow(Avro::Schema).to receive(:parse).and_call_original
123
+ expect(avro.decode(data)).to eq message
124
+ expect(Avro::Schema).not_to have_received(:parse)
125
+ end
126
+ end
127
+
128
+ it_behaves_like "encoding and decoding with the schema from schema store"
129
+
130
+ it_behaves_like 'encoding and decoding with the schema from registry'
131
+
132
+ it_behaves_like 'encoding and decoding with the schema_id from registry'
64
133
 
65
134
  context "with a provided registry" do
66
- let(:registry) { AvroTurf::SchemaRegistry.new(registry_url, logger: logger) }
135
+ let(:registry) { AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger) }
67
136
 
68
137
  let(:avro) do
69
138
  AvroTurf::Messaging.new(
@@ -73,7 +142,11 @@ describe AvroTurf::Messaging do
73
142
  )
74
143
  end
75
144
 
76
- it_behaves_like "encoding and decoding"
145
+ it_behaves_like "encoding and decoding with the schema from schema store"
146
+
147
+ it_behaves_like 'encoding and decoding with the schema from registry'
148
+
149
+ it_behaves_like 'encoding and decoding with the schema_id from registry'
77
150
 
78
151
  it "uses the provided registry" do
79
152
  allow(registry).to receive(:register).and_call_original
@@ -101,7 +174,7 @@ describe AvroTurf::Messaging do
101
174
  )
102
175
  end
103
176
 
104
- it_behaves_like "encoding and decoding"
177
+ it_behaves_like "encoding and decoding with the schema from schema store"
105
178
 
106
179
  it "uses the provided schema store" do
107
180
  allow(schema_store).to receive(:find).and_call_original
@@ -109,4 +182,119 @@ describe AvroTurf::Messaging do
109
182
  expect(schema_store).to have_received(:find).with("person", nil)
110
183
  end
111
184
  end
185
+
186
+ describe 'decoding with #decode_message' do
187
+ shared_examples_for "encoding and decoding with the schema from schema store" do
188
+ it "encodes and decodes messages" do
189
+ data = avro.encode(message, schema_name: "person")
190
+ result = avro.decode_message(data)
191
+ expect(result.message).to eq message
192
+ expect(result.schema_id).to eq 0
193
+ expect(result.writer_schema).to eq schema
194
+ expect(result.reader_schema).to eq nil
195
+ end
196
+
197
+ it "allows specifying a reader's schema" do
198
+ data = avro.encode(message, schema_name: "person")
199
+ result = avro.decode_message(data, schema_name: "person")
200
+ expect(result.message).to eq message
201
+ expect(result.writer_schema).to eq schema
202
+ expect(result.reader_schema).to eq schema
203
+ end
204
+
205
+ it "caches parsed schemas for decoding" do
206
+ data = avro.encode(message, schema_name: "person")
207
+ avro.decode_message(data)
208
+ allow(Avro::Schema).to receive(:parse).and_call_original
209
+ expect(avro.decode_message(data).message).to eq message
210
+ expect(Avro::Schema).not_to have_received(:parse)
211
+ end
212
+ end
213
+
214
+ shared_examples_for 'encoding and decoding with the schema from registry' do
215
+ before do
216
+ registry = AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger)
217
+ registry.register('person', schema)
218
+ registry.register('people', schema)
219
+ end
220
+
221
+ it 'encodes and decodes messages' do
222
+ data = avro.encode(message, subject: 'person', version: 1)
223
+ result = avro.decode_message(data)
224
+ expect(result.message).to eq message
225
+ expect(result.schema_id).to eq 0
226
+ end
227
+
228
+ it "allows specifying a reader's schema by subject and version" do
229
+ data = avro.encode(message, subject: 'person', version: 1)
230
+ expect(avro.decode_message(data, schema_name: 'person').message).to eq message
231
+ end
232
+
233
+ it 'raises AvroTurf::SchemaNotFoundError when the schema does not exist on registry' do
234
+ expect { avro.encode(message, subject: 'missing', version: 1) }.to raise_error(AvroTurf::SchemaNotFoundError)
235
+ end
236
+
237
+ it 'caches parsed schemas for decoding' do
238
+ data = avro.encode(message, subject: 'person', version: 1)
239
+ avro.decode_message(data)
240
+ allow(Avro::Schema).to receive(:parse).and_call_original
241
+ expect(avro.decode_message(data).message).to eq message
242
+ expect(Avro::Schema).not_to have_received(:parse)
243
+ end
244
+ end
245
+
246
+ it_behaves_like "encoding and decoding with the schema from schema store"
247
+
248
+ it_behaves_like 'encoding and decoding with the schema from registry'
249
+
250
+ context "with a provided registry" do
251
+ let(:registry) { AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger) }
252
+
253
+ let(:avro) do
254
+ AvroTurf::Messaging.new(
255
+ registry: registry,
256
+ schemas_path: "spec/schemas",
257
+ logger: logger
258
+ )
259
+ end
260
+
261
+ it_behaves_like "encoding and decoding with the schema from schema store"
262
+
263
+ it_behaves_like 'encoding and decoding with the schema from registry'
264
+
265
+ it "uses the provided registry" do
266
+ allow(registry).to receive(:register).and_call_original
267
+ message = { "full_name" => "John Doe" }
268
+ avro.encode(message, schema_name: "person")
269
+ expect(registry).to have_received(:register).with("person", anything)
270
+ end
271
+
272
+ it "allows specifying a schema registry subject" do
273
+ allow(registry).to receive(:register).and_call_original
274
+ message = { "full_name" => "John Doe" }
275
+ avro.encode(message, schema_name: "person", subject: "people")
276
+ expect(registry).to have_received(:register).with("people", anything)
277
+ end
278
+ end
279
+
280
+ context "with a provided schema store" do
281
+ let(:schema_store) { AvroTurf::SchemaStore.new(path: "spec/schemas") }
282
+
283
+ let(:avro) do
284
+ AvroTurf::Messaging.new(
285
+ registry_url: registry_url,
286
+ schema_store: schema_store,
287
+ logger: logger
288
+ )
289
+ end
290
+
291
+ it_behaves_like "encoding and decoding with the schema from schema store"
292
+
293
+ it "uses the provided schema store" do
294
+ allow(schema_store).to receive(:find).and_call_original
295
+ avro.encode(message, schema_name: "person")
296
+ expect(schema_store).to have_received(:find).with("person", nil)
297
+ end
298
+ end
299
+ end
112
300
  end
@@ -197,6 +197,42 @@ describe AvroTurf::SchemaStore do
197
197
  schema = store.find("person")
198
198
  expect(schema.fullname).to eq "person"
199
199
  end
200
+
201
+ it "is thread safe" do
202
+ define_schema "address.avsc", <<-AVSC
203
+ {
204
+ "type": "record",
205
+ "name": "address",
206
+ "fields": []
207
+ }
208
+ AVSC
209
+
210
+ # Set a Thread breakpoint right in the core place of race condition
211
+ expect(Avro::Name)
212
+ .to receive(:add_name)
213
+ .and_wrap_original { |m, *args|
214
+ Thread.stop
215
+ m.call(*args)
216
+ }
217
+
218
+ # Run two concurring threads which both will trigger the same schema loading
219
+ threads = 2.times.map { Thread.new { store.find("address") } }
220
+ # Wait for the moment when both threads will reach the breakpoint
221
+ sleep 0.001 until threads.all?(&:stop?)
222
+
223
+ expect {
224
+ # Resume the threads evaluation, one after one
225
+ threads.each do |thread|
226
+ next unless thread.status == 'sleep'
227
+
228
+ thread.run
229
+ sleep 0.001 until thread.stop?
230
+ end
231
+
232
+ # Ensure that threads are finished
233
+ threads.each(&:join)
234
+ }.to_not raise_error
235
+ end
200
236
  end
201
237
 
202
238
  describe "#load_schemas!" do
@@ -12,6 +12,14 @@ module Helpers
12
12
  f.write(content)
13
13
  end
14
14
  end
15
+
16
+ def store_cache(path, hash)
17
+ File.write(File.join("spec/cache", path), JSON.generate(hash))
18
+ end
19
+
20
+ def load_cache(path)
21
+ JSON.parse(File.read(File.join("spec/cache", path)))
22
+ end
15
23
  end
16
24
 
17
25
  RSpec.configure do |config|
@@ -1,6 +1,6 @@
1
1
  # This shared example expects a registry variable to be defined
2
2
  # with an instance of the registry class being tested.
3
- shared_examples_for "a schema registry client" do
3
+ shared_examples_for "a confluent schema registry client" do
4
4
  let(:logger) { Logger.new(StringIO.new) }
5
5
  let(:registry_url) { "http://registry.example.com" }
6
6
  let(:subject_name) { "some-subject" }
@@ -15,8 +15,8 @@ shared_examples_for "a schema registry client" do
15
15
  end
16
16
 
17
17
  before do
18
- stub_request(:any, /^#{registry_url}/).to_rack(FakeSchemaRegistryServer)
19
- FakeSchemaRegistryServer.clear
18
+ stub_request(:any, /^#{registry_url}/).to_rack(FakeConfluentSchemaRegistryServer)
19
+ FakeConfluentSchemaRegistryServer.clear
20
20
  end
21
21
 
22
22
  describe "#register and #fetch" do
@@ -88,16 +88,18 @@ shared_examples_for "a schema registry client" do
88
88
  end
89
89
 
90
90
  describe "#subject_version" do
91
- before do
92
- 2.times do |n|
93
- registry.register(subject_name,
94
- { type: :record, name: "r#{n}", fields: [] }.to_json)
95
- end
91
+ let!(:schema_id1) do
92
+ registry.register(subject_name, { type: :record, name: "r0", fields: [] }.to_json)
93
+ end
94
+ let!(:schema_id2) do
95
+ registry.register(subject_name, { type: :record, name: "r1", fields: [] }.to_json)
96
96
  end
97
+
97
98
  let(:expected) do
98
99
  {
99
100
  name: subject_name,
100
101
  version: 1,
102
+ id: schema_id1,
101
103
  schema: { type: :record, name: "r0", fields: [] }.to_json
102
104
  }.to_json
103
105
  end
@@ -112,6 +114,7 @@ shared_examples_for "a schema registry client" do
112
114
  {
113
115
  name: subject_name,
114
116
  version: 2,
117
+ id: schema_id2,
115
118
  schema: { type: :record, name: "r1", fields: [] }.to_json
116
119
  }.to_json
117
120
  end
@@ -178,6 +181,67 @@ shared_examples_for "a schema registry client" do
178
181
  end
179
182
  end
180
183
 
184
+ describe "#global_config" do
185
+ let(:expected) do
186
+ { compatibility: 'BACKWARD' }.to_json
187
+ end
188
+
189
+ it "returns the global configuration" do
190
+ expect(registry.global_config).to eq(JSON.parse(expected))
191
+ end
192
+ end
193
+
194
+ describe "#update_global_config" do
195
+ let(:config) do
196
+ { compatibility: 'FORWARD' }
197
+ end
198
+ let(:expected) { config.to_json }
199
+
200
+ it "updates the global configuration and returns it" do
201
+ expect(registry.update_global_config(config)).to eq(JSON.parse(expected))
202
+ expect(registry.global_config).to eq(JSON.parse(expected))
203
+ end
204
+ end
205
+
206
+ describe "#subject_config" do
207
+ let(:expected) do
208
+ { compatibility: 'BACKWARD' }.to_json
209
+ end
210
+
211
+ context "when the subject config is not set" do
212
+ it "returns the global configuration" do
213
+ expect(registry.subject_config(subject_name)).to eq(JSON.parse(expected))
214
+ end
215
+ end
216
+
217
+ context "when the subject config is set" do
218
+ let(:config) do
219
+ { compatibility: 'FULL' }
220
+ end
221
+ let(:expected) { config.to_json }
222
+
223
+ before do
224
+ registry.update_subject_config(subject_name, config)
225
+ end
226
+
227
+ it "returns the subject config" do
228
+ expect(registry.subject_config(subject_name)).to eq(JSON.parse(expected))
229
+ end
230
+ end
231
+ end
232
+
233
+ describe "#update_subject_config" do
234
+ let(:config) do
235
+ { compatibility: 'NONE' }
236
+ end
237
+ let(:expected) { config.to_json }
238
+
239
+ it "updates the subject config and returns it" do
240
+ expect(registry.update_subject_config(subject_name, config)).to eq(JSON.parse(expected))
241
+ expect(registry.subject_config(subject_name)).to eq(JSON.parse(expected))
242
+ end
243
+ end
244
+
181
245
  # Monkey patch an Avro::Schema to simulate the presence of
182
246
  # active_support/core_ext.
183
247
  def break_to_json(avro_schema)