From 0ce59376629233b8004d3ac3db35cdf73f114105 Mon Sep 17 00:00:00 2001 From: Adarsh Pandit Date: Fri, 29 Jan 2016 16:08:30 -0800 Subject: [PATCH] Initial Commit Reason for Change ================= * Create an initial commit of the work which was happening privately. Changes ======= * Add all of the files including the documentation. --- .document | 5 + .gitignore | 5 + .rdoc_options | 16 ++ .travis.yml | 18 ++ ChangeLog.md | 3 + Gemfile | 5 + LICENSE.txt | 20 ++ README.md | 200 ++++++++++++++++++ Rakefile | 40 ++++ env_configuration_for_local_gem_tests.yml | 6 + examples/.sample.env | 17 ++ fastly_nsq.gemspec | 35 +++ lib/fastly_nsq.rb | 7 + lib/fastly_nsq/fake_message_queue.rb | 58 +++++ lib/fastly_nsq/message_queue.rb | 27 +++ lib/fastly_nsq/nsq_message_queue.rb | 18 ++ lib/fastly_nsq/queue_listener.rb | 29 +++ lib/fastly_nsq/sample_message_processor.rb | 45 ++++ lib/fastly_nsq/version.rb | 3 + .../lib/fastly_nsq/fake_message_queue_test.rb | 76 +++++++ test/lib/fastly_nsq/fastly_nsq_test.rb | 10 + test/lib/fastly_nsq/message_queue_test.rb | 47 ++++ test/lib/fastly_nsq/nsq_message_queue_test.rb | 35 +++ test/lib/fastly_nsq/queue_listener_test.rb | 60 ++++++ .../sample_message_processor_test.rb | 59 ++++++ test/test_helper.rb | 50 +++++ 26 files changed, 894 insertions(+) create mode 100644 .document create mode 100644 .gitignore create mode 100644 .rdoc_options create mode 100644 .travis.yml create mode 100644 ChangeLog.md create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Rakefile create mode 100644 env_configuration_for_local_gem_tests.yml create mode 100644 examples/.sample.env create mode 100644 fastly_nsq.gemspec create mode 100644 lib/fastly_nsq.rb create mode 100644 lib/fastly_nsq/fake_message_queue.rb create mode 100644 lib/fastly_nsq/message_queue.rb create mode 100644 lib/fastly_nsq/nsq_message_queue.rb create mode 100644 lib/fastly_nsq/queue_listener.rb create mode 100644 lib/fastly_nsq/sample_message_processor.rb create mode 100644 lib/fastly_nsq/version.rb create mode 100644 test/lib/fastly_nsq/fake_message_queue_test.rb create mode 100644 test/lib/fastly_nsq/fastly_nsq_test.rb create mode 100644 test/lib/fastly_nsq/message_queue_test.rb create mode 100644 test/lib/fastly_nsq/nsq_message_queue_test.rb create mode 100644 test/lib/fastly_nsq/queue_listener_test.rb create mode 100644 test/lib/fastly_nsq/sample_message_processor_test.rb create mode 100644 test/test_helper.rb diff --git a/.document b/.document new file mode 100644 index 0000000..b05cd7a --- /dev/null +++ b/.document @@ -0,0 +1,5 @@ +lib/**/*.rb +README.md +CHANGELOG.md + +LICENSE.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2666065 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.bundle +/Gemfile.lock +/html/ +/pkg/ +/vendor/cache/*.gem diff --git a/.rdoc_options b/.rdoc_options new file mode 100644 index 0000000..c12cd0a --- /dev/null +++ b/.rdoc_options @@ -0,0 +1,16 @@ +--- !ruby/object:RDoc::Options +encoding: UTF-8 +static_path: [] +rdoc_include: + - . +charset: UTF-8 +exclude: +hyperlink_all: false +line_numbers: false +main_page: README.md +markup: markdown +show_hash: false +tab_width: 8 +title: fastly_nsq Documentation +visibility: :protected +webcvs: diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..042fa1f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: ruby +cache: bundler +rvm: + - 2.0 + - 2.1 + - 2.2 + - 2.3 +script: + - bundle exec rake test +notifications: + slack: + rooms: + - 'fastly:hFvhBQKliYl9QAO3VujcrLhe#billy' + on_success: change + on_failure: change + email: + on_success: never + on_failure: never diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..4d5c62a --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,3 @@ +### 0.1.0 / 2016-01-25 + +* Initial release: diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..a687990 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gemspec + +gem 'minitest-utils', require: false diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c54e146 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2016 Fastly, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebea6f3 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# fastly_nsq + +NSQ adapter and testing objects +for using the NSQ messaging system +in your Ruby project. + +This library is intended +to facilitate publishing and consuming +messages on an NSQ messaging queue. + +We also include a fake queue +to make testing easier. + +This library is dependent +on the [`nsq-ruby`] gem. + +[`nsq-ruby`]: https://github.com/wistia/nsq-ruby + +Please use [GitHub Issues] to report bugs. + +[GitHub Issues]: https://github.com/fastly/fastly_nsq/issues + + +## Install + +`fastly_nsq` is a Ruby Gem +tested against Rails `>= 4.2` +and Ruby `>= 2.0`. + +To get started, +add `fastly_nsq` to your `Gemfile` +and `bundle install`. + +The gem includes the following objects: + +### [`MessageQueue`] + +This is an adapter class +which takes a required `topic` string +and provides entry points +for the queue's message producer and consumer classes. + +The queue strategy used +can be switched +by adding an environment variable +to your application: + +```ruby +if ENV['FAKE_QUEUE'] == true + FakeMessageQueue +else + NsqMessageQueue +end +``` + +[`MessageQueue`]: lib/fastly_nsq/message_queue.rb + + +### [`NsqMessageQueue`] + +This strategy +creates a connection +to `nsq-ruby`'s +`Nsq::Producer` and `Nsq::Consumer` classes. + +[`NsqMessageQueue`]: lib/fastly_nsq/nsq_message_queue.rb + + +### [`FakeMessageQueue`] + +This strategy +mocks the connection +to NSQ for testing purposes. + +It adheres to the same API +as `NsqMessageQueue`. + +[`FakeMessageQueue`]: lib/fastly_nsq/fake_message_queue.rb + + +*IMPORTANT NOTE:* You must create your own `MessageProcessor` class +for this gem to work in your application. + +See more information below. + + +## Configuration + +### Processing Messages + +This gem expects you to create a +new class called `MessageProcessor` +which will process messages +once they are consumed off of the queue topic. + +This class needs to adhere to the following API: + +```ruby +class MessageProcessor + # This an instance of NSQ::Message or FakeMessageQueue::Message + def initialize(message) + @message = message + end + + def start + # Do things with the message. It's JSON body is accessible by @message.body. + + # Finish the message to let the queue know it is complete like so: + @message.finish + end +end +``` + +### Environment Variables + +The URLs for the various +NSQ endpoints are expected +in `ENV` variables. + +Below are the required variables +and sample values for using +stock NSQ on OS X, +installed via Homebrew: + +```shell +BROADCAST_ADDRESS='127.0.0.1' +NSQD_TCP_ADDRESS='127.0.0.1:4150' +NSQD_HTTP_ADDRESS='127.0.0.1:4151' +NSQLOOKUPD_TCP_ADDRESS='127.0.0.1:4160' +NSQLOOKUPD_HTTP_ADDRESS='127.0.0.1:4161' +``` + +See the [`.sample.env`](examples/.sample.env) file +for more detail. + +### Live vs. Fake + +In the gem's test suite, +the fake message queue is used. + +If you would like to force +use of the real NSQ adapter, +ensure `FAKE_QUEUE` is *not* set to anything in `ENV`. + +When you are developing your application, +it is recommended to +start by using the fake queue: + +```shell +FAKE_QUEUE=true +``` + +Also note that during gem tests, +we are aliasing `MessageProcessor` to `SampleMessageProcessor`. +You can also refer to the latter +as an example of how +you might write your own processor. + + +## How to Use the Gem +### Publishing Messages + +To publish a message on the queue: + +```ruby +message_data = { + "event_type": "heartbeat", + "data": { + "service": "Northstar" + } +} +message_string = message_data.to_json +producer = MessageQueue.new(topic: 'northstar').producer +producer.write(message_string) +``` + +### Consuming Messages + +To consume the next message on the queue: + +```ruby +# TBD!!!!! +# Waiting until I put the `QueueListener` stuff in here... +``` + +## Additional Reference + +## Acknowledgements + +* Documentation inspired by [Steve Losh's "Teach Don't Tell"] post. +* Thanks to Wistia for `nsq-ruby`. + +[Steve Losh's "Teach Don't Tell"]: http://stevelosh.com/blog/2013/09/teach-dont-tell/ + + +## Copyright + +Copyright (c) 2016 Fastly, Inc under an MIT license. + +See [LICENSE.txt](LICENSE.txt) for details. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..3be4aa8 --- /dev/null +++ b/Rakefile @@ -0,0 +1,40 @@ +# encoding: utf-8 + +require 'rubygems' + +begin + require 'bundler/setup' +rescue LoadError => e + abort e.message +end + +require 'rake' + +require 'rubygems/tasks' +Gem::Tasks.new + +require 'rdoc/task' +RDoc::Task.new +task doc: :rdoc + +require 'rake/testtask' + +Rake::TestTask.new do |test| + test.libs << 'test' + test.pattern = 'test/**/*_test.rb' + test.verbose = true +end + +require 'bundler/audit/cli' + +namespace :bundler do + desc 'Updates the ruby-advisory-db and runs audit' + task :audit do + %w(update check).each do |command| + Bundler::Audit::CLI.start [command] + end + end +end + +task(:default).clear +task default: ['test', 'bundler:audit'] diff --git a/env_configuration_for_local_gem_tests.yml b/env_configuration_for_local_gem_tests.yml new file mode 100644 index 0000000..1cb866c --- /dev/null +++ b/env_configuration_for_local_gem_tests.yml @@ -0,0 +1,6 @@ +BROADCAST_ADDRESS: '127.0.0.1' +NSQD_TCP_ADDRESS: '127.0.0.1:4150' +NSQD_HTTP_ADDRESS: '127.0.0.1:4151' +NSQLOOKUPD_TCP_ADDRESS: '127.0.0.1:4160' +NSQLOOKUPD_HTTP_ADDRESS: '127.0.0.1:4161' +FAKE_QUEUE: 'true' diff --git a/examples/.sample.env b/examples/.sample.env new file mode 100644 index 0000000..01aa7dd --- /dev/null +++ b/examples/.sample.env @@ -0,0 +1,17 @@ +# NSQ +# In the Fastly Dev VM +BROADCAST_ADDRESS='billing' +NSQD_TCP_ADDRESS='billing:1910' +NSQD_HTTP_ADDRESS='billing:1911' +NSQLOOKUPD_TCP_ADDRESS='billing:4160' +NSQLOOKUPD_HTTP_ADDRESS='billing:4161' + +# On OSX with Homebrew defaults (brew install nsq) +BROADCAST_ADDRESS='127.0.0.1' +NSQD_TCP_ADDRESS='127.0.0.1:4150' +NSQD_HTTP_ADDRESS='127.0.0.1:4151' +NSQLOOKUPD_TCP_ADDRESS='127.0.0.1:4160' +NSQLOOKUPD_HTTP_ADDRESS='127.0.0.1:4161' + +# Uncomment to test against a live queue +#LIVE_QUEUE=true diff --git a/fastly_nsq.gemspec b/fastly_nsq.gemspec new file mode 100644 index 0000000..9ee4318 --- /dev/null +++ b/fastly_nsq.gemspec @@ -0,0 +1,35 @@ +# -*- encoding: utf-8 -*- + +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'fastly_nsq/version' + +Gem::Specification.new do |gem| + gem.name = 'fastly_nsq' + gem.version = FastlyNsq::VERSION + gem.summary = 'Fastly NSQ Adapter' + gem.description = "Helper classes for Fastly's NSQ Services" + gem.license = 'MIT' + gem.authors = ["Tommy O'Neill, Adarsh Pandit"] + gem.email = 'tommy@fastly.com' + gem.homepage = 'https://github.com/fastly/fastly-nsq' + + gem.files = ['lib/fastly_nsq'] + + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|features)/}) + gem.require_paths = ['lib'] + + gem.add_development_dependency 'awesome_print', '~> 1.6' + gem.add_development_dependency 'bundler', '~> 1.10' + gem.add_development_dependency 'bundler-audit', '~> 0.4' + gem.add_development_dependency 'minitest', '~> 5.8' + gem.add_development_dependency 'pry-byebug', '~> 3.3' + gem.add_development_dependency 'rake', '~> 10.5' + gem.add_development_dependency 'rdoc', '~> 4.2' + gem.add_development_dependency 'rspec-mocks', '~> 3.4' + gem.add_development_dependency 'rubygems-tasks', '~> 0.2' + + gem.add_dependency 'activesupport', '~> 4.2', '>= 4.2.5.1' + gem.add_dependency 'nsq-ruby', '~> 1.5.0', '>= 1.5.0' +end diff --git a/lib/fastly_nsq.rb b/lib/fastly_nsq.rb new file mode 100644 index 0000000..3caa458 --- /dev/null +++ b/lib/fastly_nsq.rb @@ -0,0 +1,7 @@ +require 'fastly_nsq/version' + +require 'fastly_nsq/message_queue' +require 'fastly_nsq/fake_message_queue' +require 'fastly_nsq/nsq_message_queue' + +require 'fastly_nsq/queue_listener' diff --git a/lib/fastly_nsq/fake_message_queue.rb b/lib/fastly_nsq/fake_message_queue.rb new file mode 100644 index 0000000..f02a85c --- /dev/null +++ b/lib/fastly_nsq/fake_message_queue.rb @@ -0,0 +1,58 @@ +require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/module/introspection' + +class FakeMessageQueue + cattr_accessor :queue + + def self.producer(topic:) + @producer ||= Producer.new + end + + def self.consumer(topic:, channel:) + @consumer ||= Consumer.new + end + + def self.reset! + self.queue = [] + end + + class Producer + def write(string) + message = Message.new(string) + queue.push(message) + end + + private + + def queue + self.class.parent.queue + end + end + + class Consumer + def pop + queue.pop + end + + def size + queue.size + end + + private + + def queue + self.class.parent.queue + end + end + + class Message + attr_reader :body + + def initialize(body) + @body = body + end + + def finish + end + end +end diff --git a/lib/fastly_nsq/message_queue.rb b/lib/fastly_nsq/message_queue.rb new file mode 100644 index 0000000..af5a771 --- /dev/null +++ b/lib/fastly_nsq/message_queue.rb @@ -0,0 +1,27 @@ +class MessageQueue + CHANNEL = 'billing_app'.freeze + + def initialize(topic:) + @topic = topic + end + + def producer + @producer ||= queue.producer(topic: topic) + end + + def consumer + @consumer ||= queue.consumer(topic: topic, channel: CHANNEL) + end + + def queue + if ENV['FAKE_QUEUE'].nil? + NsqMessageQueue + else + FakeMessageQueue + end + end + + private + + attr_reader :topic +end diff --git a/lib/fastly_nsq/nsq_message_queue.rb b/lib/fastly_nsq/nsq_message_queue.rb new file mode 100644 index 0000000..4394c58 --- /dev/null +++ b/lib/fastly_nsq/nsq_message_queue.rb @@ -0,0 +1,18 @@ +require 'nsq' + +class NsqMessageQueue + def self.producer(topic:) + Nsq::Producer.new( + nsqd: ENV.fetch('NSQD_TCP_ADDRESS'), + topic: topic, + ) + end + + def self.consumer(topic:, channel:) + Nsq::Consumer.new( + nsqlookupd: ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS'), + topic: topic, + channel: channel, + ) + end +end diff --git a/lib/fastly_nsq/queue_listener.rb b/lib/fastly_nsq/queue_listener.rb new file mode 100644 index 0000000..58908d2 --- /dev/null +++ b/lib/fastly_nsq/queue_listener.rb @@ -0,0 +1,29 @@ +require 'active_support/core_ext/object/blank' + +class QueueListener + def initialize(topic:) + @queue = MessageQueue.new(topic: topic) + end + + def start + loop do + process_next_message + end + end + + def process_next_message + message = consumer.pop + + if message.present? + MessageProcessor.new(message).start + end + end + + def consumer + queue.consumer + end + + private + + attr_reader :queue +end diff --git a/lib/fastly_nsq/sample_message_processor.rb b/lib/fastly_nsq/sample_message_processor.rb new file mode 100644 index 0000000..1e9126c --- /dev/null +++ b/lib/fastly_nsq/sample_message_processor.rb @@ -0,0 +1,45 @@ +class HeartbeatWorker; end +class UnknownMessageWorker; end + +class SampleMessageProcessor + EVENT_TYPE_TO_WORKER_MAP = { + 'heartbeat' => HeartbeatWorker, + }.freeze + + def initialize(message) + @message = message + end + + def start + process_message + message.finish + end + + private + + attr_reader :message + + def process_message + message_processor.perform_async(message_data) + end + + def message_processor + EVENT_TYPE_TO_WORKER_MAP.fetch(event_type, UnknownMessageWorker) + end + + def event_type + parsed_message_body['event_type'] + end + + def message_data + parsed_message_body['data'] + end + + def parsed_message_body + JSON.parse(message_body) + end + + def message_body + message.body + end +end diff --git a/lib/fastly_nsq/version.rb b/lib/fastly_nsq/version.rb new file mode 100644 index 0000000..88c2c1b --- /dev/null +++ b/lib/fastly_nsq/version.rb @@ -0,0 +1,3 @@ +module FastlyNsq + VERSION = '0.1.0' +end diff --git a/test/lib/fastly_nsq/fake_message_queue_test.rb b/test/lib/fastly_nsq/fake_message_queue_test.rb new file mode 100644 index 0000000..2befa4f --- /dev/null +++ b/test/lib/fastly_nsq/fake_message_queue_test.rb @@ -0,0 +1,76 @@ +require 'test_helper' + +describe FakeMessageQueue do + describe '.producer' do + it 'returns an instance of the fake producer' do + producer = FakeMessageQueue.producer(topic: 'any_topic') + + assert_kind_of FakeMessageQueue::Producer, producer + end + end + + describe '.consumer' do + it 'returns an instance of the fake consumer' do + consumer = FakeMessageQueue.consumer(topic: 'any_topic', channel: 'any') + + assert_kind_of FakeMessageQueue::Consumer, consumer + end + end + + describe '.reset!' do + it 'resets the fake message queue' do + FakeMessageQueue.queue = ['hello'] + + FakeMessageQueue.reset! + + assert_empty FakeMessageQueue.queue + end + end + + describe 'Producer' do + describe '#write' do + it 'adds a new message to the queue' do + FakeMessageQueue::Producer.new.write('hello') + + assert_equal 1, FakeMessageQueue.queue.size + end + end + end + + describe 'Message' do + describe '#body' do + it 'returns the body of the message' do + content = 'hello' + FakeMessageQueue::Producer.new.write(content) + + message = FakeMessageQueue.queue.first + body = message.body + + assert_equal content, body + end + end + end + + describe 'Consumer' do + describe '#size' do + it 'tells you how many messages are in the queue' do + FakeMessageQueue.queue = ['hello'] + + queue_size = FakeMessageQueue::Consumer.new.size + + assert_equal 1, queue_size + end + end + + describe '#pop' do + it 'returns the last message off of the queue' do + message = FakeMessageQueue::Message.new('hello') + FakeMessageQueue.queue = [message] + + popped_message = FakeMessageQueue::Consumer.new.pop + + assert_equal message, popped_message + end + end + end +end diff --git a/test/lib/fastly_nsq/fastly_nsq_test.rb b/test/lib/fastly_nsq/fastly_nsq_test.rb new file mode 100644 index 0000000..212acdc --- /dev/null +++ b/test/lib/fastly_nsq/fastly_nsq_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' +require 'fastly_nsq' + +describe FastlyNsq do + it 'has a version number' do + version = FastlyNsq.const_get('VERSION') + + assert(!version.empty?, 'should have a VERSION constant') + end +end diff --git a/test/lib/fastly_nsq/message_queue_test.rb b/test/lib/fastly_nsq/message_queue_test.rb new file mode 100644 index 0000000..fac9b21 --- /dev/null +++ b/test/lib/fastly_nsq/message_queue_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' +require 'nsq' + +describe MessageQueue do + describe '#producer' do + it 'returns a connection to the fake producer with the default topic' do + allow(FakeMessageQueue).to receive(:producer) + topic = 'minitest' + + MessageQueue.new(topic: topic).producer + + expect(FakeMessageQueue).to have_received(:producer).with(topic: topic) + end + end + + describe '#consumer' do + it 'returns a connection to the fake consumer' do + allow(FakeMessageQueue).to receive(:consumer) + topic = 'minitest' + + MessageQueue.new(topic: topic).consumer + + expect(FakeMessageQueue).to have_received(:consumer). + with(topic: topic, channel: MessageQueue::CHANNEL) + end + end + + describe '#queue' do + describe 'when in using the live queue' do + it 'returns the connection to the live server' do + allow(ENV).to receive(:[]).with('FAKE_QUEUE').and_return(nil) + topic = 'minitest' + + assert_equal MessageQueue.new(topic: topic).queue, NsqMessageQueue + end + end + + describe 'when the FAKE_QUEUE ENV variable is set' do + it 'returns the connection to the fake queue' do + allow(ENV).to receive(:[]).with('FAKE_QUEUE').and_return(true) + topic = 'minitest' + + assert_equal MessageQueue.new(topic: topic).queue, FakeMessageQueue + end + end + end +end diff --git a/test/lib/fastly_nsq/nsq_message_queue_test.rb b/test/lib/fastly_nsq/nsq_message_queue_test.rb new file mode 100644 index 0000000..cc27a01 --- /dev/null +++ b/test/lib/fastly_nsq/nsq_message_queue_test.rb @@ -0,0 +1,35 @@ +require 'test_helper' + +describe NsqMessageQueue do + describe '.producer' do + it 'creates an instance of the producer' do + allow(Nsq::Producer).to receive(:new) + topic = 'minitest' + + NsqMessageQueue.producer(topic: topic) + + expect(Nsq::Producer).to have_received(:new). + with( + nsqd: ENV.fetch('NSQD_TCP_ADDRESS'), + topic: topic, + ) + end + end + + describe '.consumer' do + it 'creates an instance of the consumer' do + allow(Nsq::Consumer).to receive(:new) + topic = 'minitest' + channel = 'william' + + NsqMessageQueue.consumer(topic: topic, channel: channel) + + expect(Nsq::Consumer).to have_received(:new). + with( + nsqlookupd: ENV.fetch('NSQLOOKUPD_HTTP_ADDRESS'), + topic: topic, + channel: channel, + ) + end + end +end diff --git a/test/lib/fastly_nsq/queue_listener_test.rb b/test/lib/fastly_nsq/queue_listener_test.rb new file mode 100644 index 0000000..f692379 --- /dev/null +++ b/test/lib/fastly_nsq/queue_listener_test.rb @@ -0,0 +1,60 @@ +require 'test_helper' + +describe QueueListener do + describe '#process_next_message' do + it 'will take a passed topic' do + allow(MessageQueue).to receive_message_chain( + :new, + :consumer, + :pop, + ) + topic = 'minitest' + + QueueListener.new(topic: topic).process_next_message + + expect(MessageQueue).to have_received(:new).with(topic: topic) + end + + describe 'when there is a message' do + it 'processes the message' do + process_message = double(start: nil) + allow(MessageProcessor).to receive(:new).and_return(process_message) + message = double(present?: true) + allow(MessageQueue).to receive_message_chain( + :new, + :consumer, + pop: message + ) + topic = 'minitest' + + QueueListener.new(topic: topic).process_next_message + + expect(MessageProcessor).to have_received(:new).with(message) + expect(process_message).to have_received(:start) + end + end + + describe 'when there is no message' do + it 'does not process the message' do + missing_message = double(present?: false) + allow(MessageQueue).to receive_message_chain( + :new, + :consumer, + pop: missing_message + ) + process_message = double(start: nil) + allow(MessageProcessor).to receive(:new).and_return(process_message) + topic = 'minitest' + + QueueListener.new(topic: topic).process_next_message + + expect(MessageProcessor).not_to have_received(:new) + expect(process_message).not_to have_received(:start) + end + end + end + + describe '#start' do + # Infinite loops are untestable + end +end diff --git a/test/lib/fastly_nsq/sample_message_processor_test.rb b/test/lib/fastly_nsq/sample_message_processor_test.rb new file mode 100644 index 0000000..399fbe2 --- /dev/null +++ b/test/lib/fastly_nsq/sample_message_processor_test.rb @@ -0,0 +1,59 @@ +require 'test_helper' + +describe SampleMessageProcessor do + describe '#start' do + it 'enqueues the appropriate message processor' do + data = { 'key' => 'value' } + body = { 'event_type' => 'heartbeat', 'data' => data }.to_json + message = double('Message', body: body, finish: nil) + allow(HeartbeatWorker).to receive(:perform_async) + + SampleMessageProcessor.new(message).start + + expect(HeartbeatWorker).to have_received(:perform_async).with(data) + end + + it 'finishes the message' do + data = { 'key' => 'value' } + body = { 'event_type' => 'heartbeat', 'data' => data }.to_json + message = double('Message', body: body, finish: nil) + allow(HeartbeatWorker).to receive(:perform_async) + + SampleMessageProcessor.new(message).start + + expect(message).to have_received(:finish) + end + + describe 'when the message event_type is not known' do + it 'uses the null object processor' do + data = { 'sample_key' => 'sample value' } + body = { + 'event_type' => 'unregistered_message_type', + 'data' => data, + }.to_json + message = double('Message', body: body, finish: nil) + allow(UnknownMessageWorker).to receive(:perform_async) + + SampleMessageProcessor.new(message).start + + expect(UnknownMessageWorker).to have_received(:perform_async).with(data) + end + end + + describe 'when the message lacks an event_type' do + it 'uses the null object processor' do + data = { 'sample_key' => 'sample value' } + body = { + 'not_the_event_type_key' => 'unregistered_message_type', + 'data' => data, + }.to_json + message = double('Message', body: body, finish: nil) + allow(UnknownMessageWorker).to receive(:perform_async) + + SampleMessageProcessor.new(message).start + + expect(UnknownMessageWorker).to have_received(:perform_async).with(data) + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..be748e5 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,50 @@ +require 'rubygems' + +begin + require 'bundler/setup' +rescue LoadError => error + abort error.message +end + +require 'fastly_nsq' + +require 'minitest/autorun' +require 'awesome_print' +require 'pry-byebug' +require_relative '../lib/fastly_nsq/sample_message_processor' + +MessageProcessor = SampleMessageProcessor + +MiniTest::Spec.before do + load_sample_environment_variables + FakeMessageQueue.reset! +end + +def load_sample_environment_variables + env_file = File.open('env_configuration_for_local_gem_tests.yml') + + YAML.load(env_file).each do |key, value| + ENV[key.to_s] = value + end +end + +require 'rspec/mocks' +module MinitestRSpecMocksIntegration + include ::RSpec::Mocks::ExampleMethods + + def before_setup + ::RSpec::Mocks.setup + super + end + + def after_teardown + super + ::RSpec::Mocks.verify + ensure + ::RSpec::Mocks.teardown + end +end + +class MiniTest::Spec + include MinitestRSpecMocksIntegration +end