avro_turf 1.20.0 → 1.20.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f3d6b25c3e25a9137581fbe4bdb3e45015d371c479f8beeb54fa30ee8a3475e
4
- data.tar.gz: a59d1634303784ae309a40c45f6abf70acdd5b681abe9a89bac40fa0497c5a63
3
+ metadata.gz: 80b65f5f08285ac85c04012f6b9f1fa9483095013a36fbaa5f93fa5b6d379e33
4
+ data.tar.gz: 6cfc6bdc7b87a0c2784bed5d6451c4803466fdd0d2fc0c8fa3d8bd61546ddbce
5
5
  SHA512:
6
- metadata.gz: 85b800371ca6102518333c64732a26af7d103d35453923e1ac332b6c643e1826c00c90b826e9332c23e4de5f995bd40c8411447184289c2834fc45dbeded1664
7
- data.tar.gz: df05accd07e4876a776a6dffb1850bfecd73d82727fc22ab5d25a4e002c069d50c7d9bb0e5fe9ae23e2f528b189c49d31ad87189d0ec07f5c2dcefb707110719
6
+ metadata.gz: 921c736f0889497e0db8327b96797a34f69d64410b1edcd05d3e961116eebc1476ecc039e78c4a06b49e8b03bfc8706153e446ac07831fef20c49002333f6c5d
7
+ data.tar.gz: 74d4ca51d66e3f5aa4f232cec3e3f52b86467d20c04145d51bb4e71c861f3120897eb38934e52ca35756b2fe69f0a1c6d22dae027ea923c258a7b12faf382cec
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Memoize parsed schemas to improve encoding performance when using `AvroTurf::Messaging` (#206)
6
+
7
+ ## v1.20.1
8
+
9
+ - Remove `sinatra` as a development dependency (#237)
10
+
5
11
  ## v1.20.0
6
12
 
7
13
  - Add support for client certificate chains via `client_chain` and `client_chain_data` parameters (#233)
data/avro_turf.gemspec CHANGED
@@ -27,7 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "rspec", "~> 3.2"
28
28
  spec.add_development_dependency "fakefs", "~> 3"
29
29
  spec.add_development_dependency "webmock"
30
- spec.add_development_dependency "sinatra"
31
30
  spec.add_development_dependency "json_spec"
32
31
  spec.add_development_dependency "rack-test"
33
32
  spec.add_development_dependency "resolv"
@@ -236,13 +236,15 @@ class AvroTurf
236
236
  if schema_type && schema_type != "AVRO"
237
237
  raise IncompatibleSchemaError, "The #{schema_type} schema for #{subject} is incompatible."
238
238
  end
239
- schema = Avro::Schema.parse(schema_data.fetch("schema"))
239
+
240
+ schema = @schemas_by_id[schema_id] ||= Avro::Schema.parse(schema_data.fetch("schema"))
241
+
240
242
  [schema, schema_id]
241
243
  end
242
244
 
243
245
  # Fetch the schema from registry with the provided schema_id.
244
246
  def fetch_schema_by_id(schema_id)
245
- schema = @schemas_by_id.fetch(schema_id) do
247
+ schema = @schemas_by_id[schema_id] ||= begin
246
248
  schema_json = @registry.fetch(schema_id)
247
249
  Avro::Schema.parse(schema_json)
248
250
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sinatra/base"
3
+ require_relative "fake_server"
4
+
5
+ class FakeConfluentSchemaRegistryServer
6
+ include AvroTurf::Test::FakeServer
4
7
 
5
- class FakeConfluentSchemaRegistryServer < Sinatra::Base
6
8
  QUALIFIED_SUBJECT = /
7
9
  :(?<context>\.[^:]*)
8
10
  :(?<subject>.*)
@@ -22,22 +24,21 @@ class FakeConfluentSchemaRegistryServer < Sinatra::Base
22
24
  attr_reader :global_config
23
25
  end
24
26
 
25
- helpers do
26
- def parse_schema
27
- request.body.rewind
28
- JSON.parse(request.body.read).fetch("schema").tap do |schema|
29
- Avro::Schema.parse(schema)
30
- end
27
+ # Helper methods (previously in Sinatra helpers block)
28
+ def parse_schema
29
+ request.body.rewind
30
+ JSON.parse(request.body.read).fetch("schema").tap do |schema|
31
+ Avro::Schema.parse(schema)
31
32
  end
33
+ end
32
34
 
33
- def parse_config
34
- request.body.rewind
35
- JSON.parse(request.body.read)
36
- end
35
+ def parse_config
36
+ request.body.rewind
37
+ JSON.parse(request.body.read)
38
+ end
37
39
 
38
- def global_config
39
- self.class.global_config
40
- end
40
+ def global_config
41
+ self.class.global_config
41
42
  end
42
43
 
43
44
  post "/subjects/:qualified_subject/versions" do
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sinatra/base"
3
+ require_relative "fake_confluent_schema_registry_server"
4
4
 
5
5
  class FakePrefixedConfluentSchemaRegistryServer < FakeConfluentSchemaRegistryServer
6
6
  DEFAULT_CONTEXT = "."
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rack"
5
+
6
+ # Ensure AvroTurf class exists so we can add modules to it
7
+ # This is defined as a class (not module) in lib/avro_turf/version.rb
8
+ class AvroTurf
9
+ module Test
10
+ end
11
+ end
12
+
13
+ # A lightweight Rack-based router module that provides Sinatra-like DSL.
14
+ # This module is designed to replace Sinatra::Base for the fake schema registry servers
15
+ # used in testing, eliminating the sinatra dependency.
16
+ #
17
+ # Usage:
18
+ # class MyServer
19
+ # include AvroTurf::Test::FakeServer
20
+ #
21
+ # get "/path/:param" do
22
+ # { result: params[:param] }.to_json
23
+ # end
24
+ #
25
+ # post "/other" do
26
+ # halt(404, '{"error": "not found"}') if some_condition
27
+ # '{"ok": true}'
28
+ # end
29
+ # end
30
+ #
31
+ module AvroTurf::Test::FakeServer
32
+ def self.included(base)
33
+ base.extend(ClassMethods)
34
+ base.include(InstanceMethods)
35
+ end
36
+
37
+ module ClassMethods
38
+ # Storage for routes defined in this class
39
+ def routes
40
+ @routes ||= {"GET" => [], "POST" => [], "PUT" => [], "DELETE" => []}
41
+ end
42
+
43
+ # When a class inherits from another that includes FakeServer,
44
+ # ensure it gets its own routes hash
45
+ def inherited(subclass)
46
+ super
47
+ subclass.instance_variable_set(:@routes, nil)
48
+ end
49
+
50
+ # Define a GET route
51
+ def get(pattern, &block)
52
+ add_route("GET", pattern, block)
53
+ end
54
+
55
+ # Define a POST route
56
+ def post(pattern, &block)
57
+ add_route("POST", pattern, block)
58
+ end
59
+
60
+ # Define a PUT route
61
+ def put(pattern, &block)
62
+ add_route("PUT", pattern, block)
63
+ end
64
+
65
+ # Define a DELETE route
66
+ def delete(pattern, &block)
67
+ add_route("DELETE", pattern, block)
68
+ end
69
+
70
+ # Sinatra-compatible `set` method for configuration
71
+ def set(key, value)
72
+ case key
73
+ when :host_authorization
74
+ @host_authorization = value
75
+ else
76
+ instance_variable_set(:"@#{key}", value)
77
+ end
78
+ end
79
+
80
+ # Access host authorization settings
81
+ def host_authorization
82
+ @host_authorization
83
+ end
84
+
85
+ # Rack interface - creates a new instance and calls it
86
+ def call(env)
87
+ new.call(env)
88
+ end
89
+
90
+ private
91
+
92
+ def add_route(method, pattern, block)
93
+ routes[method] << [compile_pattern(pattern), pattern, block]
94
+ end
95
+
96
+ # Convert a route pattern like "/subjects/:subject/versions" to a regex
97
+ # with named capture groups: /^\/subjects\/(?<subject>[^\/]+)\/versions$/
98
+ def compile_pattern(pattern)
99
+ regex_str = Regexp.escape(pattern).gsub(/:(\w+)/) { "(?<#{$1}>[^/]+)" }
100
+ Regexp.new("^#{regex_str}$")
101
+ end
102
+ end
103
+
104
+ module InstanceMethods
105
+ attr_reader :request, :params
106
+
107
+ def call(env)
108
+ @request = Rack::Request.new(env)
109
+ @params = {}
110
+
111
+ # Check host authorization if configured
112
+ if (auth = self.class.host_authorization)
113
+ permitted = auth[:permitted_hosts] || []
114
+ unless permitted.include?(@request.host)
115
+ return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
116
+ end
117
+ end
118
+
119
+ # Use catch/throw for halt mechanism (like Sinatra)
120
+ catch(:halt) do
121
+ route_and_dispatch(env)
122
+ end
123
+ end
124
+
125
+ # Early return from a route handler with a specific status and body
126
+ def halt(status, body)
127
+ throw :halt, [status, {"Content-Type" => "application/json"}, [body]]
128
+ end
129
+
130
+ private
131
+
132
+ def route_and_dispatch(env)
133
+ method = env["REQUEST_METHOD"]
134
+ path = env["PATH_INFO"]
135
+
136
+ # Parse query string into params (with both string and symbol keys for compatibility)
137
+ query_params = Rack::Utils.parse_query(env["QUERY_STRING"] || "")
138
+ @params = {}
139
+ query_params.each do |key, value|
140
+ @params[key] = value
141
+ @params[key.to_sym] = value
142
+ end
143
+
144
+ # Find matching route (check own class first, then ancestors)
145
+ matched = find_route(method, path)
146
+
147
+ if matched
148
+ regex, _pattern, block = matched
149
+
150
+ # Extract path parameters from the match
151
+ if (match = regex.match(path))
152
+ match.names.each do |name|
153
+ # Store with both symbol and string keys for compatibility
154
+ @params[name.to_sym] = match[name]
155
+ @params[name] = match[name]
156
+ end
157
+ end
158
+
159
+ # Execute the route block in the context of this instance
160
+ body = instance_exec(&block)
161
+ [200, {"Content-Type" => "text/html;charset=utf-8"}, [body]]
162
+ else
163
+ [404, {"Content-Type" => "text/plain"}, ["Not Found"]]
164
+ end
165
+ end
166
+
167
+ # Find a matching route by searching this class's routes first,
168
+ # then parent classes (to support inheritance)
169
+ def find_route(method, path)
170
+ klass = self.class
171
+ while klass
172
+ if klass.respond_to?(:routes, true) && klass.routes[method]
173
+ klass.routes[method].each do |route|
174
+ regex, _, _ = route
175
+ return route if regex.match(path)
176
+ end
177
+ end
178
+ # Move up the inheritance chain
179
+ klass = klass.superclass
180
+ # Stop if we've gone past classes that include FakeServer
181
+ break unless klass.respond_to?(:routes, true)
182
+ end
183
+ nil
184
+ end
185
+ end
186
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AvroTurf
4
- VERSION = "1.20.0"
4
+ VERSION = "1.20.1"
5
5
  end
@@ -380,6 +380,17 @@ describe AvroTurf::Messaging do
380
380
  expect(subject).to eq([schema, schema_id])
381
381
  end
382
382
 
383
+ it "memoizes parsed schema" do
384
+ expect(registry).to receive(:subject_version).with(subj, version).twice.and_return(response)
385
+ expect(Avro::Schema).to receive(:parse).with(schema_json).once.and_return(schema)
386
+
387
+ first_result = avro.fetch_schema(subject: subj, version: version)
388
+ second_result = avro.fetch_schema(subject: subj, version: version)
389
+
390
+ expect(first_result).to eq([schema, schema_id])
391
+ expect(second_result).to eq([schema, schema_id])
392
+ end
393
+
383
394
  context "with an incompatible schema type" do
384
395
  let(:response) { {"id" => schema_id, "schema" => "blah", "schemaType" => schema_type} }
385
396
  let(:schema_type) { "PROTOBUF" }
@@ -403,6 +414,16 @@ describe AvroTurf::Messaging do
403
414
  it "gets schema from registry" do
404
415
  expect(subject).to eq([schema, schema_id])
405
416
  end
417
+
418
+ it "memoizes schema in @schemas_by_id after first call" do
419
+ expect(registry).to receive(:fetch).with(schema_id).once.and_return(schema_json)
420
+
421
+ first_result = avro.fetch_schema_by_id(schema_id)
422
+ second_result = avro.fetch_schema_by_id(schema_id)
423
+
424
+ expect(first_result).to eq([schema, schema_id])
425
+ expect(second_result).to eq([schema, schema_id])
426
+ end
406
427
  end
407
428
 
408
429
  context "using fetch_schema_by_body" do
@@ -0,0 +1,722 @@
1
+ # frozen_string_literal: true
2
+
3
+ # HTTP Contract Tests for FakeConfluentSchemaRegistryServer
4
+ #
5
+ # These tests verify the exact HTTP behavior of the fake schema registry server,
6
+ # including status codes, headers, and response body structure.
7
+ # They serve as a specification for the Rack-based replacement of Sinatra.
8
+
9
+ require "rack/test"
10
+
11
+ RSpec.describe "FakeConfluentSchemaRegistryServer HTTP Contract" do
12
+ include Rack::Test::Methods
13
+
14
+ def app
15
+ AuthorizedFakeConfluentSchemaRegistryServer
16
+ end
17
+
18
+ before do
19
+ # Must call clear on the actual app class to reset the global_config class instance variable
20
+ AuthorizedFakeConfluentSchemaRegistryServer.clear
21
+ end
22
+
23
+ def schema(name: "test_schema")
24
+ {
25
+ type: "record",
26
+ name: name,
27
+ fields: [
28
+ {name: "name", type: "string"}
29
+ ]
30
+ }.to_json
31
+ end
32
+
33
+ def json_content_type
34
+ "application/vnd.schemaregistry+json"
35
+ end
36
+
37
+ def post_json(path, body)
38
+ post path, body.to_json, "CONTENT_TYPE" => json_content_type
39
+ end
40
+
41
+ def put_json(path, body)
42
+ put path, body.to_json, "CONTENT_TYPE" => json_content_type
43
+ end
44
+
45
+ describe "Response Headers" do
46
+ # Note: Sinatra defaults to text/html, we'll preserve this behavior
47
+ # but our Rack replacement could potentially improve this
48
+ it "returns a content type for successful requests" do
49
+ post_json "/subjects/test/versions", {schema: schema}
50
+
51
+ # Sinatra defaults to text/html;charset=utf-8
52
+ expect(last_response.content_type).to be_truthy
53
+ end
54
+
55
+ it "returns a content type for error responses" do
56
+ get "/schemas/ids/999"
57
+
58
+ expect(last_response.content_type).to be_truthy
59
+ end
60
+ end
61
+
62
+ describe "POST /subjects/:subject/versions" do
63
+ it "returns 200 status for successful registration" do
64
+ post_json "/subjects/test-subject/versions", {schema: schema}
65
+
66
+ expect(last_response.status).to eq(200)
67
+ end
68
+
69
+ it "returns JSON with 'id' key" do
70
+ post_json "/subjects/test-subject/versions", {schema: schema}
71
+
72
+ body = JSON.parse(last_response.body)
73
+ expect(body).to have_key("id")
74
+ expect(body["id"]).to be_a(Integer)
75
+ end
76
+
77
+ it "returns same id for same schema in same subject" do
78
+ post_json "/subjects/test-subject/versions", {schema: schema}
79
+ first_id = JSON.parse(last_response.body)["id"]
80
+
81
+ post_json "/subjects/test-subject/versions", {schema: schema}
82
+ second_id = JSON.parse(last_response.body)["id"]
83
+
84
+ expect(second_id).to eq(first_id)
85
+ end
86
+
87
+ it "returns same id for same schema in different subject" do
88
+ post_json "/subjects/subject1/versions", {schema: schema}
89
+ first_id = JSON.parse(last_response.body)["id"]
90
+
91
+ post_json "/subjects/subject2/versions", {schema: schema}
92
+ second_id = JSON.parse(last_response.body)["id"]
93
+
94
+ expect(second_id).to eq(first_id)
95
+ end
96
+
97
+ it "returns different id for different schema" do
98
+ post_json "/subjects/test-subject/versions", {schema: schema(name: "schema1")}
99
+ first_id = JSON.parse(last_response.body)["id"]
100
+
101
+ post_json "/subjects/test-subject/versions", {schema: schema(name: "schema2")}
102
+ second_id = JSON.parse(last_response.body)["id"]
103
+
104
+ expect(second_id).not_to eq(first_id)
105
+ end
106
+
107
+ context "with schema context" do
108
+ it "supports qualified subject names" do
109
+ post_json "/subjects/:.context1:test/versions", {schema: schema}
110
+
111
+ expect(last_response.status).to eq(200)
112
+ body = JSON.parse(last_response.body)
113
+ expect(body).to have_key("id")
114
+ end
115
+
116
+ it "isolates schemas by context" do
117
+ post_json "/subjects/:.ctx1:test/versions", {schema: schema(name: "s1")}
118
+ id1 = JSON.parse(last_response.body)["id"]
119
+
120
+ post_json "/subjects/:.ctx2:test/versions", {schema: schema(name: "s2")}
121
+ id2 = JSON.parse(last_response.body)["id"]
122
+
123
+ # Different contexts start from 0
124
+ expect(id1).to eq(id2)
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "GET /schemas/ids/:schema_id" do
130
+ it "returns 200 status for existing schema" do
131
+ post_json "/subjects/test/versions", {schema: schema}
132
+ schema_id = JSON.parse(last_response.body)["id"]
133
+
134
+ get "/schemas/ids/#{schema_id}"
135
+
136
+ expect(last_response.status).to eq(200)
137
+ end
138
+
139
+ it "returns JSON with 'schema' key containing the schema JSON" do
140
+ test_schema = schema(name: "my_schema")
141
+ post_json "/subjects/test/versions", {schema: test_schema}
142
+ schema_id = JSON.parse(last_response.body)["id"]
143
+
144
+ get "/schemas/ids/#{schema_id}"
145
+
146
+ body = JSON.parse(last_response.body)
147
+ expect(body).to have_key("schema")
148
+ expect(body["schema"]).to eq(test_schema)
149
+ end
150
+
151
+ it "returns 404 status for non-existent schema" do
152
+ get "/schemas/ids/999"
153
+
154
+ expect(last_response.status).to eq(404)
155
+ end
156
+
157
+ it "returns error JSON for non-existent schema" do
158
+ get "/schemas/ids/999"
159
+
160
+ body = JSON.parse(last_response.body)
161
+ expect(body["error_code"]).to eq(40403)
162
+ expect(body["message"]).to eq("Schema not found")
163
+ end
164
+
165
+ context "with schema context" do
166
+ it "fetches schema from specified context via query param" do
167
+ test_schema = schema(name: "ctx_schema")
168
+ post_json "/subjects/:.myctx:test/versions", {schema: test_schema}
169
+ schema_id = JSON.parse(last_response.body)["id"]
170
+
171
+ get "/schemas/ids/#{schema_id}?subject=:.myctx:"
172
+
173
+ expect(last_response.status).to eq(200)
174
+ body = JSON.parse(last_response.body)
175
+ expect(body["schema"]).to eq(test_schema)
176
+ end
177
+ end
178
+ end
179
+
180
+ describe "GET /schemas/ids/:schema_id/versions" do
181
+ it "returns 200 status for existing schema" do
182
+ post_json "/subjects/test/versions", {schema: schema}
183
+ schema_id = JSON.parse(last_response.body)["id"]
184
+
185
+ get "/schemas/ids/#{schema_id}/versions"
186
+
187
+ expect(last_response.status).to eq(200)
188
+ end
189
+
190
+ it "returns array of subject/version objects" do
191
+ post_json "/subjects/test-subject/versions", {schema: schema}
192
+ schema_id = JSON.parse(last_response.body)["id"]
193
+
194
+ get "/schemas/ids/#{schema_id}/versions"
195
+
196
+ body = JSON.parse(last_response.body)
197
+ expect(body).to be_an(Array)
198
+ expect(body.first).to have_key("subject")
199
+ expect(body.first).to have_key("version")
200
+ expect(body.first["subject"]).to eq("test-subject")
201
+ expect(body.first["version"]).to eq(1)
202
+ end
203
+
204
+ it "returns all subjects using the schema" do
205
+ test_schema = schema(name: "shared")
206
+ post_json "/subjects/subject1/versions", {schema: test_schema}
207
+ post_json "/subjects/subject2/versions", {schema: test_schema}
208
+ schema_id = JSON.parse(last_response.body)["id"]
209
+
210
+ get "/schemas/ids/#{schema_id}/versions"
211
+
212
+ body = JSON.parse(last_response.body)
213
+ subjects = body.map { |v| v["subject"] }
214
+ expect(subjects).to include("subject1", "subject2")
215
+ end
216
+
217
+ it "returns 404 for non-existent schema" do
218
+ get "/schemas/ids/999/versions"
219
+
220
+ expect(last_response.status).to eq(404)
221
+ end
222
+ end
223
+
224
+ describe "GET /subjects" do
225
+ it "returns 200 status" do
226
+ get "/subjects"
227
+
228
+ expect(last_response.status).to eq(200)
229
+ end
230
+
231
+ it "returns empty array when no subjects" do
232
+ get "/subjects"
233
+
234
+ body = JSON.parse(last_response.body)
235
+ expect(body).to eq([])
236
+ end
237
+
238
+ it "returns array of subject names" do
239
+ post_json "/subjects/subject1/versions", {schema: schema(name: "s1")}
240
+ post_json "/subjects/subject2/versions", {schema: schema(name: "s2")}
241
+
242
+ get "/subjects"
243
+
244
+ body = JSON.parse(last_response.body)
245
+ expect(body).to include("subject1", "subject2")
246
+ end
247
+
248
+ it "includes subjects from all contexts" do
249
+ post_json "/subjects/plain-subject/versions", {schema: schema(name: "s1")}
250
+ post_json "/subjects/:.ctx:context-subject/versions", {schema: schema(name: "s2")}
251
+
252
+ get "/subjects"
253
+
254
+ body = JSON.parse(last_response.body)
255
+ expect(body).to include("plain-subject")
256
+ expect(body).to include(":.ctx:context-subject")
257
+ end
258
+ end
259
+
260
+ describe "GET /subjects/:subject/versions" do
261
+ it "returns 200 status for existing subject" do
262
+ post_json "/subjects/test/versions", {schema: schema}
263
+
264
+ get "/subjects/test/versions"
265
+
266
+ expect(last_response.status).to eq(200)
267
+ end
268
+
269
+ it "returns array of version numbers" do
270
+ post_json "/subjects/test/versions", {schema: schema(name: "v1")}
271
+ post_json "/subjects/test/versions", {schema: schema(name: "v2")}
272
+
273
+ get "/subjects/test/versions"
274
+
275
+ body = JSON.parse(last_response.body)
276
+ expect(body).to eq([1, 2])
277
+ end
278
+
279
+ it "returns 404 for non-existent subject" do
280
+ get "/subjects/nonexistent/versions"
281
+
282
+ expect(last_response.status).to eq(404)
283
+ end
284
+
285
+ it "returns error JSON for non-existent subject" do
286
+ get "/subjects/nonexistent/versions"
287
+
288
+ body = JSON.parse(last_response.body)
289
+ expect(body["error_code"]).to eq(40401)
290
+ expect(body["message"]).to eq("Subject not found")
291
+ end
292
+ end
293
+
294
+ describe "GET /subjects/:subject/versions/:version" do
295
+ before do
296
+ post_json "/subjects/test/versions", {schema: schema(name: "version1")}
297
+ @schema1 = schema(name: "version1")
298
+ @id1 = JSON.parse(last_response.body)["id"]
299
+
300
+ post_json "/subjects/test/versions", {schema: schema(name: "version2")}
301
+ @schema2 = schema(name: "version2")
302
+ @id2 = JSON.parse(last_response.body)["id"]
303
+ end
304
+
305
+ it "returns 200 status for existing version" do
306
+ get "/subjects/test/versions/1"
307
+
308
+ expect(last_response.status).to eq(200)
309
+ end
310
+
311
+ it "returns full schema details" do
312
+ get "/subjects/test/versions/1"
313
+
314
+ body = JSON.parse(last_response.body)
315
+ expect(body["subject"]).to eq("test")
316
+ expect(body["version"]).to eq(1)
317
+ expect(body["id"]).to eq(@id1)
318
+ expect(body["schema"]).to eq(@schema1)
319
+ end
320
+
321
+ it "returns correct version when version number specified" do
322
+ get "/subjects/test/versions/2"
323
+
324
+ body = JSON.parse(last_response.body)
325
+ expect(body["version"]).to eq(2)
326
+ expect(body["id"]).to eq(@id2)
327
+ expect(body["schema"]).to eq(@schema2)
328
+ end
329
+
330
+ it "supports 'latest' as version" do
331
+ get "/subjects/test/versions/latest"
332
+
333
+ body = JSON.parse(last_response.body)
334
+ expect(body["version"]).to eq(2)
335
+ expect(body["schema"]).to eq(@schema2)
336
+ end
337
+
338
+ it "returns 404 for non-existent subject" do
339
+ get "/subjects/nonexistent/versions/1"
340
+
341
+ expect(last_response.status).to eq(404)
342
+ body = JSON.parse(last_response.body)
343
+ expect(body["error_code"]).to eq(40401)
344
+ end
345
+
346
+ it "returns 404 for non-existent version" do
347
+ get "/subjects/test/versions/99"
348
+
349
+ expect(last_response.status).to eq(404)
350
+ body = JSON.parse(last_response.body)
351
+ expect(body["error_code"]).to eq(40402)
352
+ expect(body["message"]).to eq("Version not found")
353
+ end
354
+ end
355
+
356
+ describe "POST /subjects/:subject (check schema)" do
357
+ before do
358
+ @test_schema = schema(name: "registered")
359
+ post_json "/subjects/test/versions", {schema: @test_schema}
360
+ @schema_id = JSON.parse(last_response.body)["id"]
361
+ end
362
+
363
+ it "returns 200 status for registered schema" do
364
+ post_json "/subjects/test", {schema: @test_schema}
365
+
366
+ expect(last_response.status).to eq(200)
367
+ end
368
+
369
+ it "returns schema details for registered schema" do
370
+ post_json "/subjects/test", {schema: @test_schema}
371
+
372
+ body = JSON.parse(last_response.body)
373
+ expect(body["subject"]).to eq("test")
374
+ expect(body["id"]).to eq(@schema_id)
375
+ expect(body["version"]).to eq(1)
376
+ expect(body["schema"]).to eq(@test_schema)
377
+ end
378
+
379
+ it "returns 404 for unregistered schema" do
380
+ post_json "/subjects/test", {schema: schema(name: "unregistered")}
381
+
382
+ expect(last_response.status).to eq(404)
383
+ body = JSON.parse(last_response.body)
384
+ expect(body["error_code"]).to eq(40403)
385
+ end
386
+ end
387
+
388
+ describe "GET /config" do
389
+ it "returns 200 status" do
390
+ get "/config"
391
+
392
+ expect(last_response.status).to eq(200)
393
+ end
394
+
395
+ it "returns default global config" do
396
+ get "/config"
397
+
398
+ body = JSON.parse(last_response.body)
399
+ expect(body["compatibility"]).to eq("BACKWARD")
400
+ end
401
+ end
402
+
403
+ describe "PUT /config" do
404
+ it "returns 200 status" do
405
+ put_json "/config", {compatibility: "FULL"}
406
+
407
+ expect(last_response.status).to eq(200)
408
+ end
409
+
410
+ it "updates and returns the new config" do
411
+ put_json "/config", {compatibility: "FULL"}
412
+
413
+ body = JSON.parse(last_response.body)
414
+ expect(body["compatibility"]).to eq("FULL")
415
+ end
416
+
417
+ it "persists the updated config" do
418
+ put_json "/config", {compatibility: "NONE"}
419
+
420
+ get "/config"
421
+ body = JSON.parse(last_response.body)
422
+ expect(body["compatibility"]).to eq("NONE")
423
+ end
424
+ end
425
+
426
+ describe "GET /config/:subject" do
427
+ it "returns 200 status" do
428
+ get "/config/test-subject"
429
+
430
+ expect(last_response.status).to eq(200)
431
+ end
432
+
433
+ it "returns global config when subject config not set" do
434
+ get "/config/test-subject"
435
+
436
+ body = JSON.parse(last_response.body)
437
+ expect(body["compatibility"]).to eq("BACKWARD")
438
+ end
439
+
440
+ it "returns subject-specific config when set" do
441
+ put_json "/config/test-subject", {compatibility: "FORWARD"}
442
+
443
+ get "/config/test-subject"
444
+
445
+ body = JSON.parse(last_response.body)
446
+ expect(body["compatibility"]).to eq("FORWARD")
447
+ end
448
+ end
449
+
450
+ describe "PUT /config/:subject" do
451
+ it "returns 200 status" do
452
+ put_json "/config/test-subject", {compatibility: "FORWARD"}
453
+
454
+ expect(last_response.status).to eq(200)
455
+ end
456
+
457
+ it "updates and returns the subject config" do
458
+ put_json "/config/test-subject", {compatibility: "FORWARD"}
459
+
460
+ body = JSON.parse(last_response.body)
461
+ expect(body["compatibility"]).to eq("FORWARD")
462
+ end
463
+
464
+ it "does not affect global config" do
465
+ put_json "/config/test-subject", {compatibility: "NONE"}
466
+
467
+ get "/config"
468
+ body = JSON.parse(last_response.body)
469
+ expect(body["compatibility"]).to eq("BACKWARD")
470
+ end
471
+
472
+ it "does not affect other subjects" do
473
+ put_json "/config/subject1", {compatibility: "NONE"}
474
+
475
+ get "/config/subject2"
476
+ body = JSON.parse(last_response.body)
477
+ expect(body["compatibility"]).to eq("BACKWARD")
478
+ end
479
+ end
480
+
481
+ describe "clear class method" do
482
+ it "resets all state" do
483
+ post_json "/subjects/test/versions", {schema: schema}
484
+ put_json "/config", {compatibility: "NONE"}
485
+
486
+ # Must call clear on the same class used as app
487
+ AuthorizedFakeConfluentSchemaRegistryServer.clear
488
+
489
+ get "/subjects"
490
+ expect(JSON.parse(last_response.body)).to eq([])
491
+
492
+ get "/config"
493
+ expect(JSON.parse(last_response.body)["compatibility"]).to eq("BACKWARD")
494
+ end
495
+ end
496
+ end
497
+
498
+ RSpec.describe "FakePrefixedConfluentSchemaRegistryServer HTTP Contract" do
499
+ include Rack::Test::Methods
500
+
501
+ def app
502
+ AuthorizedFakePrefixedConfluentSchemaRegistryServer
503
+ end
504
+
505
+ before do
506
+ # Must call clear on the actual app class to reset the global_config class instance variable
507
+ AuthorizedFakePrefixedConfluentSchemaRegistryServer.clear
508
+ end
509
+
510
+ def schema(name: "test_schema")
511
+ {
512
+ type: "record",
513
+ name: name,
514
+ fields: [
515
+ {name: "name", type: "string"}
516
+ ]
517
+ }.to_json
518
+ end
519
+
520
+ def json_content_type
521
+ "application/vnd.schemaregistry+json"
522
+ end
523
+
524
+ def post_json(path, body)
525
+ post path, body.to_json, "CONTENT_TYPE" => json_content_type
526
+ end
527
+
528
+ def put_json(path, body)
529
+ put path, body.to_json, "CONTENT_TYPE" => json_content_type
530
+ end
531
+
532
+ describe "prefixed routes" do
533
+ describe "POST /prefix/subjects/:subject/versions" do
534
+ it "returns 200 status and schema id" do
535
+ post_json "/prefix/subjects/test/versions", {schema: schema}
536
+
537
+ expect(last_response.status).to eq(200)
538
+ body = JSON.parse(last_response.body)
539
+ expect(body).to have_key("id")
540
+ end
541
+ end
542
+
543
+ describe "GET /prefix/schemas/ids/:schema_id" do
544
+ it "returns schema by id" do
545
+ post_json "/prefix/subjects/test/versions", {schema: schema}
546
+ schema_id = JSON.parse(last_response.body)["id"]
547
+
548
+ get "/prefix/schemas/ids/#{schema_id}"
549
+
550
+ expect(last_response.status).to eq(200)
551
+ body = JSON.parse(last_response.body)
552
+ expect(body).to have_key("schema")
553
+ end
554
+
555
+ it "returns 404 for non-existent schema" do
556
+ get "/prefix/schemas/ids/999"
557
+
558
+ expect(last_response.status).to eq(404)
559
+ end
560
+ end
561
+
562
+ describe "GET /prefix/subjects" do
563
+ it "returns list of subjects" do
564
+ post_json "/prefix/subjects/test1/versions", {schema: schema(name: "s1")}
565
+ post_json "/prefix/subjects/test2/versions", {schema: schema(name: "s2")}
566
+
567
+ get "/prefix/subjects"
568
+
569
+ expect(last_response.status).to eq(200)
570
+ body = JSON.parse(last_response.body)
571
+ expect(body).to include("test1", "test2")
572
+ end
573
+ end
574
+
575
+ describe "GET /prefix/subjects/:subject/versions" do
576
+ it "returns version list" do
577
+ post_json "/prefix/subjects/test/versions", {schema: schema(name: "v1")}
578
+ post_json "/prefix/subjects/test/versions", {schema: schema(name: "v2")}
579
+
580
+ get "/prefix/subjects/test/versions"
581
+
582
+ expect(last_response.status).to eq(200)
583
+ body = JSON.parse(last_response.body)
584
+ expect(body).to eq([1, 2])
585
+ end
586
+
587
+ it "returns 404 for non-existent subject" do
588
+ get "/prefix/subjects/nonexistent/versions"
589
+
590
+ expect(last_response.status).to eq(404)
591
+ end
592
+ end
593
+
594
+ describe "GET /prefix/subjects/:subject/versions/:version" do
595
+ it "returns schema details" do
596
+ test_schema = schema(name: "versioned")
597
+ post_json "/prefix/subjects/test/versions", {schema: test_schema}
598
+ schema_id = JSON.parse(last_response.body)["id"]
599
+
600
+ get "/prefix/subjects/test/versions/1"
601
+
602
+ expect(last_response.status).to eq(200)
603
+ body = JSON.parse(last_response.body)
604
+ expect(body["name"]).to eq("test")
605
+ expect(body["version"]).to eq(1)
606
+ expect(body["id"]).to eq(schema_id)
607
+ expect(body["schema"]).to eq(test_schema)
608
+ end
609
+
610
+ it "supports 'latest' version" do
611
+ post_json "/prefix/subjects/test/versions", {schema: schema(name: "v1")}
612
+ post_json "/prefix/subjects/test/versions", {schema: schema(name: "v2")}
613
+
614
+ get "/prefix/subjects/test/versions/latest"
615
+
616
+ expect(last_response.status).to eq(200)
617
+ body = JSON.parse(last_response.body)
618
+ expect(body["version"]).to eq(2)
619
+ end
620
+ end
621
+
622
+ describe "POST /prefix/subjects/:subject (check schema)" do
623
+ it "returns schema details for registered schema" do
624
+ test_schema = schema(name: "check")
625
+ post_json "/prefix/subjects/test/versions", {schema: test_schema}
626
+ schema_id = JSON.parse(last_response.body)["id"]
627
+
628
+ post_json "/prefix/subjects/test", {schema: test_schema}
629
+
630
+ expect(last_response.status).to eq(200)
631
+ body = JSON.parse(last_response.body)
632
+ expect(body["subject"]).to eq("test")
633
+ expect(body["id"]).to eq(schema_id)
634
+ end
635
+
636
+ it "returns 404 for unregistered schema" do
637
+ post_json "/prefix/subjects/test/versions", {schema: schema(name: "one")}
638
+
639
+ post_json "/prefix/subjects/test", {schema: schema(name: "other")}
640
+
641
+ expect(last_response.status).to eq(404)
642
+ end
643
+ end
644
+
645
+ describe "GET /prefix/config" do
646
+ it "returns global config" do
647
+ get "/prefix/config"
648
+
649
+ expect(last_response.status).to eq(200)
650
+ body = JSON.parse(last_response.body)
651
+ expect(body["compatibility"]).to eq("BACKWARD")
652
+ end
653
+ end
654
+
655
+ describe "PUT /prefix/config" do
656
+ it "updates global config" do
657
+ put_json "/prefix/config", {compatibility: "FULL"}
658
+
659
+ expect(last_response.status).to eq(200)
660
+ body = JSON.parse(last_response.body)
661
+ expect(body["compatibility"]).to eq("FULL")
662
+ end
663
+ end
664
+
665
+ describe "GET /prefix/config/:subject" do
666
+ it "returns subject config or global default" do
667
+ get "/prefix/config/test-subject"
668
+
669
+ expect(last_response.status).to eq(200)
670
+ body = JSON.parse(last_response.body)
671
+ expect(body["compatibility"]).to eq("BACKWARD")
672
+ end
673
+ end
674
+
675
+ describe "PUT /prefix/config/:subject" do
676
+ it "updates subject config" do
677
+ put_json "/prefix/config/test-subject", {compatibility: "NONE"}
678
+
679
+ expect(last_response.status).to eq(200)
680
+ body = JSON.parse(last_response.body)
681
+ expect(body["compatibility"]).to eq("NONE")
682
+ end
683
+ end
684
+ end
685
+ end
686
+
687
+ RSpec.describe "Host Authorization" do
688
+ include Rack::Test::Methods
689
+
690
+ def schema
691
+ {
692
+ type: "record",
693
+ name: "test",
694
+ fields: [{name: "name", type: "string"}]
695
+ }.to_json
696
+ end
697
+
698
+ describe "AuthorizedFakeConfluentSchemaRegistryServer" do
699
+ def app
700
+ AuthorizedFakeConfluentSchemaRegistryServer
701
+ end
702
+
703
+ it "allows requests from permitted hosts" do
704
+ # The default Rack::Test host is "example.org" which is in the permitted list
705
+ get "/subjects"
706
+
707
+ expect(last_response.status).to eq(200)
708
+ end
709
+ end
710
+
711
+ describe "AuthorizedFakePrefixedConfluentSchemaRegistryServer" do
712
+ def app
713
+ AuthorizedFakePrefixedConfluentSchemaRegistryServer
714
+ end
715
+
716
+ it "allows requests from permitted hosts" do
717
+ get "/prefix/subjects"
718
+
719
+ expect(last_response.status).to eq(200)
720
+ end
721
+ end
722
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avro_turf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.20.0
4
+ version: 1.20.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
@@ -119,20 +119,6 @@ dependencies:
119
119
  - - ">="
120
120
  - !ruby/object:Gem::Version
121
121
  version: '0'
122
- - !ruby/object:Gem::Dependency
123
- name: sinatra
124
- requirement: !ruby/object:Gem::Requirement
125
- requirements:
126
- - - ">="
127
- - !ruby/object:Gem::Version
128
- version: '0'
129
- type: :development
130
- prerelease: false
131
- version_requirements: !ruby/object:Gem::Requirement
132
- requirements:
133
- - - ">="
134
- - !ruby/object:Gem::Version
135
- version: '0'
136
122
  - !ruby/object:Gem::Dependency
137
123
  name: json_spec
138
124
  requirement: !ruby/object:Gem::Requirement
@@ -217,6 +203,7 @@ files:
217
203
  - lib/avro_turf/test/fake_confluent_schema_registry_server.rb
218
204
  - lib/avro_turf/test/fake_prefixed_confluent_schema_registry_server.rb
219
205
  - lib/avro_turf/test/fake_schema_registry_server.rb
206
+ - lib/avro_turf/test/fake_server.rb
220
207
  - lib/avro_turf/version.rb
221
208
  - perf/address.avsc
222
209
  - perf/encoding_size.rb
@@ -244,6 +231,7 @@ files:
244
231
  - spec/support/authorized_fake_confluent_schema_registry_server.rb
245
232
  - spec/support/authorized_fake_prefixed_confluent_schema_registry_server.rb
246
233
  - spec/support/confluent_schema_registry_context.rb
234
+ - spec/test/fake_confluent_schema_registry_server_http_contract_spec.rb
247
235
  - spec/test/fake_confluent_schema_registry_server_spec.rb
248
236
  homepage: https://github.com/dasch/avro_turf
249
237
  licenses: