nulogy_message_bus_producer 3.2.0 → 4.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -25
  3. data/Rakefile +2 -4
  4. data/db/migrate/20200611150212_create_public_subscriptions_and_events_tables.rb +2 -2
  5. data/db/migrate/20210330204121_add_event_type_to_subscription_events.rb +9 -0
  6. data/lib/nulogy_message_bus_producer.rb +32 -12
  7. data/lib/nulogy_message_bus_producer/base_subscription.rb +1 -1
  8. data/lib/nulogy_message_bus_producer/config.rb +25 -1
  9. data/lib/nulogy_message_bus_producer/configuration/query_parser.rb +71 -0
  10. data/lib/nulogy_message_bus_producer/repopulate_replication_slots.rb +7 -5
  11. data/lib/nulogy_message_bus_producer/self_serve_subscription.rb +18 -0
  12. data/lib/nulogy_message_bus_producer/subscription_event.rb +3 -0
  13. data/lib/nulogy_message_bus_producer/subscriptions/configured_subscription.rb +14 -0
  14. data/lib/nulogy_message_bus_producer/subscriptions/finder.rb +40 -0
  15. data/lib/nulogy_message_bus_producer/subscriptions/no_variables.rb +43 -0
  16. data/lib/nulogy_message_bus_producer/subscriptions/postgres_transport.rb +11 -4
  17. data/lib/nulogy_message_bus_producer/subscriptions/query_validator.rb +47 -0
  18. data/lib/nulogy_message_bus_producer/subscriptions/risky_subscription_blocker.rb +17 -10
  19. data/lib/nulogy_message_bus_producer/subscriptions/valid_for_schema_validator.rb +14 -0
  20. data/lib/nulogy_message_bus_producer/version.rb +1 -1
  21. data/lib/tasks/engine/message_bus_producer.rake +4 -4
  22. data/spec/dummy/Rakefile +1 -1
  23. data/spec/dummy/app/mailers/application_mailer.rb +2 -2
  24. data/spec/dummy/bin/bundle +2 -2
  25. data/spec/dummy/bin/rails +3 -3
  26. data/spec/dummy/bin/rake +2 -2
  27. data/spec/dummy/bin/setup +10 -11
  28. data/spec/dummy/bin/update +10 -10
  29. data/spec/dummy/bin/yarn +6 -8
  30. data/spec/dummy/config.ru +1 -1
  31. data/spec/dummy/config/boot.rb +3 -3
  32. data/spec/dummy/config/database.yml +2 -2
  33. data/spec/dummy/config/environment.rb +1 -1
  34. data/spec/dummy/config/environments/development.rb +2 -2
  35. data/spec/dummy/config/environments/production.rb +5 -5
  36. data/spec/dummy/config/environments/test.rb +2 -2
  37. data/spec/dummy/config/initializers/assets.rb +2 -2
  38. data/spec/dummy/config/puma.rb +3 -3
  39. data/spec/dummy/config/spring.rb +2 -2
  40. data/spec/dummy/db/schema.rb +2 -1
  41. data/spec/dummy/log/development.log +321 -0
  42. data/spec/dummy/log/test.log +19061 -0
  43. data/spec/integration/lib/nulogy_message_bus_producer/config_spec.rb +37 -0
  44. data/spec/integration/lib/nulogy_message_bus_producer/repopulate_replication_slots_spec.rb +23 -14
  45. data/spec/integration/lib/nulogy_message_bus_producer/subscription_spec.rb +3 -59
  46. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/finder_spec.rb +54 -0
  47. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/no_variables_spec.rb +46 -0
  48. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/postgres_transport_spec.rb +99 -55
  49. data/spec/integration/lib/nulogy_message_bus_producer/{subscriber_graphql_schema_validator_spec.rb → subscriptions/query_validator_spec.rb} +5 -5
  50. data/spec/integration/lib/nulogy_message_bus_producer/subscriptions/risky_subscription_blocker_spec.rb +1 -19
  51. data/spec/integration/lib/nulogy_message_bus_producer_spec.rb +25 -0
  52. data/spec/integration_spec_helper.rb +0 -6
  53. data/spec/nulogy_message_bus_producer/configuration/query_parser_spec.rb +58 -0
  54. data/spec/nulogy_message_bus_producer/subscriptions/subscription_spec.rb +9 -0
  55. data/spec/spec_helper.rb +25 -1
  56. data/spec/support/kafka.rb +15 -9
  57. data/spec/support/kafka_connect.rb +1 -1
  58. data/spec/support/shared_examples/subscription_validations.rb +77 -0
  59. data/spec/support/spec_utils.rb +3 -2
  60. data/spec/support/sql_helpers.rb +9 -11
  61. data/spec/support/subscription_helpers.rb +22 -4
  62. data/spec/support/test_graphql_schema.rb +7 -0
  63. metadata +58 -38
  64. data/lib/nulogy_message_bus_producer/subscriber_graphql_schema_validator.rb +0 -45
  65. data/lib/nulogy_message_bus_producer/subscription.rb +0 -28
@@ -1,12 +1,12 @@
1
1
  require "integration_spec_helper"
2
2
 
3
- RSpec.describe NulogyMessageBusProducer::SubscriberGraphqlSchemaValidator do
4
- subject(:validator) { NulogyMessageBusProducer::SubscriberGraphqlSchemaValidator.new }
3
+ RSpec.describe NulogyMessageBusProducer::Subscriptions::QueryValidator do
4
+ subject(:validator) { described_class.new }
5
5
 
6
6
  describe "#validate" do
7
7
  context "when a valid query is present" do
8
8
  it "return true" do
9
- subscribe_to(query: <<~GRAPHQL)
9
+ self_serve_subscription(query: <<~GRAPHQL)
10
10
  foo {
11
11
  id
12
12
  }
@@ -18,13 +18,13 @@ RSpec.describe NulogyMessageBusProducer::SubscriberGraphqlSchemaValidator do
18
18
 
19
19
  context "when an invalid query is present" do
20
20
  let(:subscription_with_error) do
21
- subscription = subscribe_to(query: <<~GRAPHQL)
21
+ subscription = self_serve_subscription(query: <<~GRAPHQL)
22
22
  foo {
23
23
  id
24
24
  }
25
25
  GRAPHQL
26
26
 
27
- subscription.query.gsub!(/\bid\b/, 'a_field_that_does_not_exist')
27
+ subscription.query.gsub!(/\bid\b/, "a_field_that_does_not_exist")
28
28
  subscription.save(validate: false)
29
29
  subscription
30
30
  end
@@ -17,24 +17,8 @@ RSpec.describe NulogyMessageBusProducer::Subscriptions::RiskySubscriptionBlocker
17
17
  )
18
18
  end
19
19
 
20
- it "blocks subscriptions which would expand lists" do
21
- query = <<~GRAPHQL
22
- fooList {
23
- id
24
- }
25
- GRAPHQL
26
-
27
- result = attempt_subscription(query)
28
-
29
- expect(result).to include_json(
30
- errors: [{
31
- message: "Lists may not be queried:\nfooList"
32
- }]
33
- )
34
- end
35
-
36
20
  def attempt_subscription(query)
37
- gql = <<~GRAPHQL
21
+ execute_graphql(<<~GRAPHQL, NulogyMessageBusProducer::Specs::TestSchema)
38
22
  subscription {
39
23
  testCreated (
40
24
  subscriptionId: "#{SecureRandom.uuid}",
@@ -45,7 +29,5 @@ RSpec.describe NulogyMessageBusProducer::Subscriptions::RiskySubscriptionBlocker
45
29
  }
46
30
  }
47
31
  GRAPHQL
48
-
49
- execute_graphql(gql, NulogyMessageBusProducer::Specs::TestSchema)
50
32
  end
51
33
  end
@@ -0,0 +1,25 @@
1
+ require "integration_spec_helper"
2
+
3
+ RSpec.describe NulogyMessageBusProducer do
4
+ describe "publishing events" do
5
+ it "publishes an event" do
6
+ company_uuid = SecureRandom.uuid
7
+
8
+ described_class.publish(
9
+ topic: "some-topic",
10
+ type: "some-type",
11
+ company_uuid: company_uuid,
12
+ data: {
13
+ field: "value"
14
+ }
15
+ )
16
+
17
+ message = NulogyMessageBusProducer::SubscriptionEvent.last
18
+
19
+ expect(message).to be_present
20
+ expect(message.topic_name).to eq("some-topic")
21
+ expect(message.company_uuid).to eq(company_uuid)
22
+ expect(message.event_json).to eq({"field" => "value"})
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,3 @@
1
- # Make all rspec configuration changes to this file.
2
- # Leave automatically generated configuration files untouched to facilitate gem upgrades.
3
-
4
1
  ENV["RAILS_ENV"] ||= "test"
5
2
 
6
3
  require File.expand_path("dummy/config/environment.rb", __dir__)
@@ -10,8 +7,6 @@ require "rspec/json_expectations"
10
7
  require "active_record"
11
8
  require "spec_helper"
12
9
 
13
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
14
-
15
10
  RSpec.configure do |config|
16
11
  # Uncomment this line to see full backtraces for spec failures
17
12
  # config.backtrace_exclusion_patterns = []
@@ -37,5 +32,4 @@ RSpec.configure do |config|
37
32
 
38
33
  config.include(SpecUtils)
39
34
  config.include(SqlHelpers)
40
- config.include(SubscriptionHelpers)
41
35
  end
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+
3
+ module NulogyMessageBusProducer
4
+ module Configuration
5
+ RSpec.describe QueryParser do
6
+ it "parses queries" do
7
+ query = <<~QUERY
8
+ subscription {
9
+ testCreated(subscriptionId: "abc", subscriptionGroupId: "123", topicName: "test-topic") {
10
+ foo {
11
+ id
12
+ }
13
+ }
14
+ }
15
+ QUERY
16
+
17
+ qp = QueryParser.new(query)
18
+
19
+ expect(qp.subscription_id).to eq("abc")
20
+ expect(qp.subscription_group_id).to eq("123")
21
+ expect(qp.event_type).to eq("testCreated")
22
+ expect(qp.topic).to eq("test-topic")
23
+ end
24
+
25
+ context "when query is invalid" do
26
+ it "raises errors for event type" do
27
+ query = <<~QUERY
28
+ testCreated(subscriptionId: "abc", subscriptionGroupId: "123", topicName: "test-topic") {
29
+ foo {
30
+ id
31
+ }
32
+ }
33
+ QUERY
34
+
35
+ qp = QueryParser.new(query)
36
+
37
+ expect { qp.event_type }.to raise_error QueryParser::ParseError, /Error extracting event type/
38
+ end
39
+
40
+ it "raises errors for topic" do
41
+ query = <<~QUERY
42
+ subscription {
43
+ testCreated(subscriptionId: "abc", subscriptionGroupId: "123") {
44
+ foo {
45
+ id
46
+ }
47
+ }
48
+ }
49
+ QUERY
50
+
51
+ qp = QueryParser.new(query)
52
+
53
+ expect { qp.topic }.to raise_error QueryParser::ParseError, /Error extracting topic/
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ module NulogyMessageBusProducer
4
+ module Subscriptions
5
+ RSpec.describe ConfiguredSubscription do
6
+ include_examples "subscription validations"
7
+ end
8
+ end
9
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,8 @@
1
- # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
1
+ require "rails/all"
2
+ require "nulogy_message_bus_producer"
3
+
4
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
5
+
2
6
  RSpec.configure do |config|
3
7
  config.expect_with(:rspec) do |expectations|
4
8
  # This is generally recommended, and will default to `true` in RSpec 4.
@@ -32,4 +36,24 @@ RSpec.configure do |config|
32
36
  # test failures related to randomization by passing the same `--seed` value
33
37
  # as the one that triggered the failure.
34
38
  Kernel.srand(config.seed)
39
+
40
+ config.around do |example|
41
+ if example.metadata[:subscriptions]
42
+ old_config = NulogyMessageBusProducer.config
43
+
44
+ NulogyMessageBusProducer.config = NulogyMessageBusProducer::Config.new
45
+ NulogyMessageBusProducer.config.register_schema(
46
+ schema: "NulogyMessageBusProducer::Specs::TestSchema",
47
+ key: "test"
48
+ )
49
+
50
+ example.run
51
+
52
+ NulogyMessageBusProducer.config = old_config
53
+ else
54
+ example.run
55
+ end
56
+ end
57
+
58
+ config.include(SubscriptionHelpers)
35
59
  end
@@ -24,7 +24,7 @@ module Kafka
24
24
  end
25
25
 
26
26
  def test_bootstrap_servers
27
- "localhost:9092"
27
+ "kafka:29093"
28
28
  end
29
29
 
30
30
  def setup_kafka_producer
@@ -33,6 +33,8 @@ module Kafka
33
33
 
34
34
  def setup_kafka_consumer(topic_name)
35
35
  consumer = kafka_config.consumer
36
+ puts "Subscribing to #{topic_name}"
37
+ sleep 10
36
38
  consumer.subscribe(topic_name)
37
39
  wait_for_assignment(consumer)
38
40
  consumer
@@ -44,11 +46,9 @@ module Kafka
44
46
  loop do
45
47
  message = consumer.poll(timeout)
46
48
 
47
- if message
48
- messages << message
49
- else
50
- return messages
51
- end
49
+ return messages unless message
50
+
51
+ messages << message
52
52
  end
53
53
  end
54
54
 
@@ -62,15 +62,21 @@ module Kafka
62
62
  end
63
63
 
64
64
  def create_topic(topic_name)
65
- run("docker-compose exec -T kafka kafka-topics --zookeeper zookeeper:2181 --create --replication-factor 1 --partitions 3 --if-not-exists --topic #{topic_name}")
65
+ cmd = [
66
+ "kaf topic create #{topic_name}",
67
+ "--brokers kafka:29093",
68
+ "--replicas 1",
69
+ "--partitions 3"
70
+ ]
71
+ run(cmd.join(" "))
66
72
  end
67
73
 
68
74
  def delete_topic(topic_name)
69
- run("docker-compose exec -T kafka kafka-topics --zookeeper zookeeper:2181 --delete --topic #{topic_name}")
75
+ run("kaf topic delete #{topic_name} --brokers kafka:29093")
70
76
  end
71
77
 
72
78
  def list_topics
73
- topics = run("docker-compose exec -T kafka kafka-topics --zookeeper zookeeper:2181 --list")
79
+ topics = run("kaf topics --brokers kafka:29093")
74
80
  topics.split(" ")
75
81
  end
76
82
 
@@ -28,4 +28,4 @@ class KafkaConnect
28
28
  response = @http.request(request)
29
29
  JSON.parse(response.body, symbolize_names: true)
30
30
  end
31
- end
31
+ end
@@ -0,0 +1,77 @@
1
+ RSpec.shared_examples "subscription validations" do
2
+ context "when validating" do
3
+ it "is invalid without an id" do
4
+ model = build_subscription(id: "")
5
+
6
+ model.validate
7
+
8
+ expect(model.errors[:id]).to contain_exactly("can't be blank")
9
+ end
10
+
11
+ it "is invalid without a subscription_group_id" do
12
+ model = build_subscription(subscription_group_id: "")
13
+
14
+ model.validate
15
+
16
+ expect(model.errors[:subscription_group_id]).to contain_exactly("can't be blank")
17
+ end
18
+
19
+ it "is invalid with a blank query" do
20
+ model = build_subscription(query: "")
21
+
22
+ model.validate
23
+
24
+ expect(model.errors[:query]).to contain_exactly("can't be blank")
25
+ end
26
+
27
+ it "is invalid with blank schema_key" do
28
+ model = build_subscription(schema_key: "")
29
+
30
+ model.validate
31
+
32
+ expect(model.errors[:schema_key]).to contain_exactly("can't be blank")
33
+ end
34
+
35
+ it "is invalid with an invalid schema_key" do
36
+ model = build_subscription(schema_key: "invalid")
37
+
38
+ model.validate
39
+
40
+ expect(model.errors[:query]).to contain_exactly(/Could not find a schema for schema_key 'invalid'/)
41
+ end
42
+
43
+ it "is invalid with an invalid query" do
44
+ model = build_subscription(
45
+ query: subscription_query(query: "foo { a_field_that_does_not_exist }")
46
+ )
47
+
48
+ model.validate
49
+
50
+ expect(model).not_to be_valid
51
+ expect(model.errors[:query]).to contain_exactly(
52
+ match(/Field 'a_field_that_does_not_exist' doesn't exist on type 'testObject'/)
53
+ )
54
+ end
55
+
56
+ it "valid with a valid query" do
57
+ model = build_subscription(
58
+ query: subscription_query(query: "foo { id }")
59
+ )
60
+
61
+ model.validate
62
+
63
+ expect(model.errors).not_to include(:query)
64
+ end
65
+ end
66
+
67
+ def build_subscription(overrides = {})
68
+ attrs = {
69
+ id: SecureRandom.uuid,
70
+ subscription_group_id: SecureRandom.uuid,
71
+ schema_key: "test",
72
+ query: subscription_query
73
+ }.merge(overrides)
74
+
75
+ described_class.new(attrs)
76
+ end
77
+ end
@@ -4,12 +4,13 @@ module SpecUtils
4
4
  def wait_for(attempts: 100, interval: 0.1)
5
5
  attempts.times do
6
6
  return if yield
7
+
7
8
  sleep interval
8
9
  end
9
10
  raise "Waited for #{attempts} times but it never resolved"
10
11
  end
11
12
 
12
13
  def uuid(entity_number)
13
- sprintf("00000000-0000-0000-0000-%12.12d", entity_number)
14
+ format("00000000-0000-0000-0000-%<id>12.12d", id: entity_number)
14
15
  end
15
- end
16
+ end
@@ -1,11 +1,9 @@
1
1
  module SqlHelpers
2
2
  def without_transaction
3
- begin
4
- ActiveRecord::Base.connection.rollback_transaction
5
- yield
6
- ensure
7
- truncate_db
8
- end
3
+ ActiveRecord::Base.connection.rollback_transaction
4
+ yield
5
+ ensure
6
+ truncate_db
9
7
  end
10
8
 
11
9
  def truncate_db
@@ -16,10 +14,10 @@ module SqlHelpers
16
14
 
17
15
  # Just incase these models weren't loaded, do them explicitly
18
16
  ActiveRecord::Base.connection.execute("TRUNCATE #{NulogyMessageBusProducer::SubscriptionEvent.table_name}")
19
- ActiveRecord::Base.connection.execute("TRUNCATE #{NulogyMessageBusProducer::Subscription.table_name}")
17
+ ActiveRecord::Base.connection.execute("TRUNCATE #{NulogyMessageBusProducer::SelfServeSubscription.table_name}")
20
18
  end
21
19
 
22
- def get_replication_slots
20
+ def replication_slots
23
21
  results = ActiveRecord::Base.connection.exec_query(<<~SQL)
24
22
  SELECT slot_name FROM pg_replication_slots
25
23
  SQL
@@ -35,13 +33,13 @@ module SqlHelpers
35
33
 
36
34
  def wait_for_replication_slot(slot_name)
37
35
  wait_for do
38
- get_replication_slots.any? { |replication_slot| replication_slot == slot_name }
36
+ replication_slots.any? { |replication_slot| replication_slot == slot_name }
39
37
  end
40
38
  end
41
39
 
42
40
  def wait_for_replication_slot_cleanup(slot_name)
43
41
  wait_for do
44
- get_replication_slots.none? { |replication_slot| replication_slot == slot_name }
42
+ replication_slots.none? { |replication_slot| replication_slot == slot_name }
45
43
  end
46
44
  end
47
- end
45
+ end
@@ -1,17 +1,35 @@
1
1
  module SubscriptionHelpers
2
- def subscribe_to(
2
+ def self_serve_subscription(
3
3
  schema: NulogyMessageBusProducer::Specs::TestSchema,
4
4
  subscription_id: SecureRandom.uuid,
5
5
  **query_args
6
6
  )
7
7
  gql = subscription_query(subscription_id: subscription_id, **query_args)
8
8
 
9
- expect do
9
+ expect {
10
10
  gql_response = execute_graphql(gql, schema)
11
11
  expect(gql_response).to eq(data: {})
12
- end.to change(NulogyMessageBusProducer::Subscription, :count).by(1)
12
+ }.to change(NulogyMessageBusProducer::SelfServeSubscription, :count).by(1)
13
13
 
14
- NulogyMessageBusProducer::Subscription.find(subscription_id)
14
+ NulogyMessageBusProducer::SelfServeSubscription.find(subscription_id)
15
+ end
16
+
17
+ def configured_subscription(
18
+ schema: NulogyMessageBusProducer::Specs::TestSchema,
19
+ subscription_id: SecureRandom.uuid,
20
+ subscription_group_id: SecureRandom.uuid,
21
+ **query_args
22
+ )
23
+ gql = subscription_query(
24
+ subscription_id: subscription_id,
25
+ subscription_group_id: subscription_group_id,
26
+ **query_args
27
+ )
28
+
29
+ NulogyMessageBusProducer.config.add_subscription!(
30
+ schema: schema.name,
31
+ query: gql
32
+ )
15
33
  end
16
34
 
17
35
  def subscription_query(