avro_turf 1.19.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +20 -11
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile +5 -2
  5. data/Rakefile +2 -1
  6. data/avro_turf.gemspec +16 -17
  7. data/lib/avro_turf/cached_confluent_schema_registry.rb +9 -8
  8. data/lib/avro_turf/cached_schema_registry.rb +3 -1
  9. data/lib/avro_turf/confluent_schema_registry.rb +23 -17
  10. data/lib/avro_turf/core_ext/date.rb +2 -0
  11. data/lib/avro_turf/core_ext/enumerable.rb +2 -0
  12. data/lib/avro_turf/core_ext/false_class.rb +2 -0
  13. data/lib/avro_turf/core_ext/hash.rb +4 -2
  14. data/lib/avro_turf/core_ext/nil_class.rb +2 -0
  15. data/lib/avro_turf/core_ext/numeric.rb +2 -0
  16. data/lib/avro_turf/core_ext/string.rb +2 -0
  17. data/lib/avro_turf/core_ext/symbol.rb +2 -0
  18. data/lib/avro_turf/core_ext/time.rb +2 -0
  19. data/lib/avro_turf/core_ext/true_class.rb +2 -0
  20. data/lib/avro_turf/core_ext.rb +12 -10
  21. data/lib/avro_turf/disk_cache.rb +13 -12
  22. data/lib/avro_turf/in_memory_cache.rb +2 -0
  23. data/lib/avro_turf/messaging.rb +25 -15
  24. data/lib/avro_turf/mutable_schema_store.rb +25 -4
  25. data/lib/avro_turf/schema_registry.rb +3 -1
  26. data/lib/avro_turf/schema_store.rb +3 -2
  27. data/lib/avro_turf/schema_to_avro_patch.rb +14 -12
  28. data/lib/avro_turf/test/fake_confluent_schema_registry_server.rb +39 -37
  29. data/lib/avro_turf/test/fake_prefixed_confluent_schema_registry_server.rb +12 -10
  30. data/lib/avro_turf/test/fake_schema_registry_server.rb +3 -1
  31. data/lib/avro_turf/test/fake_server.rb +186 -0
  32. data/lib/avro_turf/version.rb +3 -1
  33. data/lib/avro_turf.rb +15 -13
  34. data/perf/encoding_size.rb +4 -2
  35. data/perf/encoding_speed.rb +4 -2
  36. data/spec/avro_turf_spec.rb +24 -23
  37. data/spec/cached_confluent_schema_registry_spec.rb +9 -7
  38. data/spec/confluent_schema_registry_spec.rb +31 -10
  39. data/spec/core_ext/date_spec.rb +2 -0
  40. data/spec/core_ext/enumerable_spec.rb +2 -0
  41. data/spec/core_ext/false_class_spec.rb +2 -0
  42. data/spec/core_ext/hash_spec.rb +3 -1
  43. data/spec/core_ext/nil_class_spec.rb +2 -0
  44. data/spec/core_ext/numeric_spec.rb +2 -0
  45. data/spec/core_ext/string_spec.rb +2 -0
  46. data/spec/core_ext/symbol_spec.rb +2 -0
  47. data/spec/core_ext/time_spec.rb +2 -0
  48. data/spec/core_ext/true_class_spec.rb +2 -0
  49. data/spec/disk_cached_confluent_schema_registry_spec.rb +23 -21
  50. data/spec/messaging_spec.rb +145 -99
  51. data/spec/mutable_schema_store_spec.rb +134 -0
  52. data/spec/schema_store_spec.rb +23 -21
  53. data/spec/schema_to_avro_patch_spec.rb +8 -7
  54. data/spec/spec_helper.rb +9 -9
  55. data/spec/support/authorized_fake_confluent_schema_registry_server.rb +4 -2
  56. data/spec/support/authorized_fake_prefixed_confluent_schema_registry_server.rb +4 -2
  57. data/spec/support/confluent_schema_registry_context.rb +32 -30
  58. data/spec/test/fake_confluent_schema_registry_server_http_contract_spec.rb +722 -0
  59. data/spec/test/fake_confluent_schema_registry_server_spec.rb +97 -94
  60. metadata +7 -40
@@ -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