├── lib ├── fake_sqs │ ├── version.rb │ ├── server.rb │ ├── queue_factory.rb │ ├── show_output.rb │ ├── catch_errors.rb │ ├── actions │ │ ├── delete_queue.rb │ │ ├── purge_queue.rb │ │ ├── delete_message.rb │ │ ├── get_queue_url.rb │ │ ├── create_queue.rb │ │ ├── list_queues.rb │ │ ├── send_message.rb │ │ ├── change_message_visibility.rb │ │ ├── get_queue_attributes.rb │ │ ├── set_queue_attributes.rb │ │ ├── receive_message.rb │ │ ├── delete_message_batch.rb │ │ └── send_message_batch.rb │ ├── responder.rb │ ├── collection_view.rb │ ├── memory_database.rb │ ├── daemonize.rb │ ├── message.rb │ ├── error_responses.yml │ ├── web_interface.rb │ ├── error_response.rb │ ├── file_database.rb │ ├── queues.rb │ ├── api.rb │ ├── test_integration.rb │ └── queue.rb └── fake_sqs.rb ├── .rspec ├── .travis.yml ├── spec ├── spec_helper.rb ├── unit │ ├── queue_factory_spec.rb │ ├── show_output_spec.rb │ ├── collection_view_spec.rb │ ├── responder_spec.rb │ ├── catch_errors_spec.rb │ ├── message_spec.rb │ ├── api_spec.rb │ ├── error_response_spec.rb │ ├── queues_spec.rb │ └── queue_spec.rb ├── integration_spec_helper.rb └── acceptance │ ├── queue_actions_spec.rb │ └── message_actions_spec.rb ├── Gemfile ├── .gitignore ├── Rakefile ├── MIT-LICENSE.txt ├── fake_sqs.gemspec ├── bin └── fake_sqs └── README.md /lib/fake_sqs/version.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | VERSION = "0.3.1" 3 | end 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --order random 4 | --require spec_helper 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.2.2 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.disable_monkey_patching! 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in fake_sqs.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /lib/fake_sqs/server.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | class Server 3 | 4 | attr_reader :host, :port 5 | 6 | def initialize(options) 7 | @host = options.fetch(:host) 8 | @port = options.fetch(:port) 9 | end 10 | 11 | def url_for(queue_id) 12 | "http://#{host}:#{port}/#{queue_id}" 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/fake_sqs/queue_factory.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | class QueueFactory 3 | 4 | attr_reader :message_factory, :queue 5 | 6 | def initialize(options = {}) 7 | @message_factory = options.fetch(:message_factory) 8 | @queue = options.fetch(:queue) 9 | end 10 | 11 | def new(options) 12 | queue.new(options.merge(:message_factory => message_factory)) 13 | end 14 | 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/fake_sqs/show_output.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'yaml' 3 | 4 | module FakeSQS 5 | class ShowOutput 6 | 7 | def initialize(app) 8 | @app = app 9 | end 10 | 11 | def call(env) 12 | request = Rack::Request.new(env) 13 | result = @app.call(env) 14 | puts request.params.to_yaml 15 | puts 16 | puts(*result.last) 17 | result 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fake_sqs/catch_errors.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module FakeSQS 4 | class CatchErrors 5 | 6 | def initialize(app, options = {}) 7 | @app = app 8 | @response = options.fetch(:response) 9 | end 10 | 11 | def call(env) 12 | @app.call(env) 13 | rescue => error 14 | response = @response.new(error) 15 | [ response.status, {}, [ response.body ] ] 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/delete_queue.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class DeleteQueue 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | @queues.delete(name, params) 13 | @responder.call :DeleteQueue 14 | end 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/purge_queue.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class PurgeQueue 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | queue = @queues.get(name) 13 | queue.reset() 14 | @responder.call :PurgeQueue 15 | end 16 | 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/queue_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/queue_factory' 2 | 3 | RSpec.describe FakeSQS::QueueFactory do 4 | 5 | it "builds queues with a message factory" do 6 | message_factory = double :message_factory 7 | queue = double :queue 8 | queue_factory = FakeSQS::QueueFactory.new(message_factory: message_factory, queue: queue) 9 | expect(queue).to receive(:new).with(message_factory: message_factory, name: "Foo") 10 | queue_factory.new(name: "Foo") 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/fake_sqs/responder.rb: -------------------------------------------------------------------------------- 1 | require 'builder' 2 | require 'securerandom' 3 | 4 | module FakeSQS 5 | class Responder 6 | 7 | def call(name, &block) 8 | xml = Builder::XmlMarkup.new(:indent => 4) 9 | xml.tag! "#{name}Response" do 10 | if block 11 | xml.tag! "#{name}Result" do 12 | yield xml 13 | end 14 | end 15 | xml.ResponseMetadata do 16 | xml.RequestId SecureRandom.uuid 17 | end 18 | end 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/fake_sqs/collection_view.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module FakeSQS 4 | class CollectionView 5 | include Enumerable 6 | extend Forwardable 7 | def_delegators :@original, :[], :each, :empty?, :size, :length 8 | 9 | UnmodifiableObjectError = Class.new(StandardError) 10 | 11 | def initialize( original ) 12 | @original = original 13 | end 14 | 15 | def []=(key_or_index,value) 16 | raise UnmodifiableObjectError.new("This is a collection view and can not be modified - #{key_or_index} => #{value}") 17 | end 18 | 19 | end 20 | end -------------------------------------------------------------------------------- /lib/fake_sqs/actions/delete_message.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class DeleteMessage 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | queue = @queues.get(name) 13 | 14 | receipt = params.fetch("ReceiptHandle") 15 | queue.delete_message(receipt) 16 | @responder.call :DeleteMessage 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fake_sqs/memory_database.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | require "thread" 3 | 4 | module FakeSQS 5 | class MemoryDatabase 6 | extend Forwardable 7 | 8 | def_delegators :@queues, 9 | :[], :[]=, :delete, :each, :select, :values 10 | 11 | def initialize 12 | @semaphore = Mutex.new 13 | end 14 | 15 | def load 16 | @queues = {} 17 | end 18 | 19 | def transaction 20 | @semaphore.synchronize do 21 | yield 22 | end 23 | end 24 | 25 | def reset 26 | @queues = {} 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/show_output_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/show_output' 2 | 3 | RSpec.describe FakeSQS::ShowOutput do 4 | 5 | after do 6 | $stdout = STDOUT 7 | end 8 | 9 | it "outputs the result of rack app" do 10 | app = double :app 11 | $stdout = StringIO.new 12 | middleware = FakeSQS::ShowOutput.new(app) 13 | env = {"rack.input" => ""} 14 | expect(app).to receive(:call).with(env).and_return([200, {}, [""]]) 15 | 16 | middleware.call(env) 17 | 18 | $stdout.rewind 19 | expect($stdout.read).to eq "--- {}\n\n\n" 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/get_queue_url.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class GetQueueUrl 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(params) 12 | name = params.fetch("QueueName") 13 | queue = @queues.get(name, params) 14 | @responder.call :GetQueueUrl do |xml| 15 | xml.QueueUrl @server.url_for(queue.name) 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/create_queue.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class CreateQueue 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(params) 12 | name = params.fetch("QueueName") 13 | queue = @queues.create(name, params) 14 | @responder.call :CreateQueue do |xml| 15 | xml.QueueUrl @server.url_for(queue.name) 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/list_queues.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class ListQueues 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(params) 12 | found = @queues.list(params) 13 | @responder.call :ListQueues do |xml| 14 | found.each do |queue| 15 | xml.QueueUrl @server.url_for(queue.name) 16 | end 17 | end 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/send_message.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class SendMessage 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | queue = @queues.get(name) 13 | message = queue.send_message(params) 14 | @responder.call :SendMessage do |xml| 15 | xml.MD5OfMessageBody message.md5 16 | xml.MessageId message.id 17 | end 18 | end 19 | 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/change_message_visibility.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class ChangeMessageVisibility 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(queue, params) 12 | visibility = params.fetch("VisibilityTimeout") 13 | receipt = params.fetch("ReceiptHandle") 14 | 15 | @queues.get(queue).change_message_visibility( receipt, visibility.to_i ) 16 | @responder.call :ChangeMessageVisibility 17 | end 18 | 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/fake_sqs/actions/get_queue_attributes.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class GetQueueAttributes 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(queue_name, params) 12 | queue = @queues.get(queue_name) 13 | @responder.call :GetQueueAttributes do |xml| 14 | queue.attributes.each do |name, value| 15 | xml.Attribute do 16 | xml.Name name 17 | xml.Value value 18 | end 19 | end 20 | end 21 | end 22 | 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/set_queue_attributes.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class SetQueueAttributes 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(queue_name, params) 12 | queue = @queues.get(queue_name) 13 | results = {} 14 | params.each do |key, value| 15 | if key =~ /\AAttribute\.(\d+)\.Name\z/ 16 | results[value] = params.fetch("Attribute.#{$1}.Value") 17 | end 18 | end 19 | queue.add_queue_attributes(results) 20 | @queues.save(queue) 21 | @responder.call :SetQueueAttributes 22 | end 23 | 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/fake_sqs/daemonize.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | class Daemonize 3 | 4 | attr_reader :pid 5 | 6 | def initialize(options) 7 | @pid = options.fetch(:pid) { 8 | warn "No PID file specified while daemonizing!" 9 | exit 1 10 | } 11 | end 12 | 13 | def call 14 | Process.daemon(true, true) 15 | 16 | if File.exist?(pid) 17 | existing_pid = File.open(pid, 'r').read.chomp.to_i 18 | running = Process.getpgid(existing_pid) rescue false 19 | if running 20 | warn "Error, Process #{existing_pid} already running" 21 | exit 1 22 | else 23 | warn "Cleaning up stale pid at #{pid}" 24 | end 25 | end 26 | File.open(pid, 'w') { |f| f.write(Process.pid) } 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/receive_message.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class ReceiveMessage 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | queue = @queues.get(name) 13 | messages = queue.receive_message(params) 14 | @responder.call :ReceiveMessage do |xml| 15 | messages.each do |receipt, message| 16 | xml.Message do 17 | xml.MessageId message.id 18 | xml.ReceiptHandle receipt 19 | xml.MD5OfBody message.md5 20 | xml.Body message.body 21 | end 22 | end 23 | end 24 | end 25 | 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require "tempfile" 4 | require 'rspec/core/rake_task' 5 | 6 | namespace :spec do 7 | 8 | desc "Run only unit specs" 9 | RSpec::Core::RakeTask.new(:unit) do |t| 10 | t.pattern = "spec/unit" 11 | end 12 | 13 | desc "Run specs with in-memory database" 14 | RSpec::Core::RakeTask.new(:memory) do |t| 15 | ENV["SQS_DATABASE"] = ":memory:" 16 | t.pattern = "spec/acceptance" 17 | end 18 | 19 | desc "Run specs with file database" 20 | RSpec::Core::RakeTask.new(:file) do |t| 21 | file = Tempfile.new(["rspec-sqs", ".yml"], encoding: "utf-8") 22 | ENV["SQS_DATABASE"] = file.path 23 | t.pattern = "spec/acceptance" 24 | end 25 | 26 | end 27 | 28 | desc "Run spec suite with both in-memory and file" 29 | task :spec => ["spec:unit", "spec:memory", "spec:file"] 30 | 31 | task :default => :spec 32 | -------------------------------------------------------------------------------- /lib/fake_sqs/message.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module FakeSQS 4 | class Message 5 | 6 | attr_reader :body, :id, :md5 7 | attr_accessor :visibility_timeout 8 | 9 | def initialize(options = {}) 10 | @body = options.fetch("MessageBody") 11 | @id = options.fetch("Id") { SecureRandom.uuid } 12 | @md5 = options.fetch("MD5") { Digest::MD5.hexdigest(@body) } 13 | end 14 | 15 | def expire! 16 | self.visibility_timeout = nil 17 | end 18 | 19 | def expired?( limit = Time.now ) 20 | self.visibility_timeout.nil? || self.visibility_timeout < limit 21 | end 22 | 23 | def expire_at(seconds) 24 | self.visibility_timeout = Time.now + seconds 25 | end 26 | 27 | def attributes 28 | { 29 | "MessageBody" => body, 30 | "Id" => id, 31 | "MD5" => md5, 32 | } 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/integration_spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "aws-sdk" 2 | require "fake_sqs/test_integration" 3 | 4 | # Aws.config[:credentials] = { 5 | # :use_ssl => false, 6 | # :sqs_endpoint => "localhost", 7 | # :sqs_port => 4568, 8 | # :access_key_id => "fake access key", 9 | # :secret_access_key => "fake secret key", 10 | # } 11 | Aws.config.update( 12 | region: "us-east-1", 13 | credentials: Aws::Credentials.new("fake", "fake"), 14 | ) 15 | 16 | db = ENV["SQS_DATABASE"] || ":memory:" 17 | puts "\n\e[34mRunning specs with database \e[33m#{db}\e[0m" 18 | 19 | $fake_sqs = FakeSQS::TestIntegration.new( 20 | database: db, 21 | sqs_endpoint: "localhost", 22 | sqs_port: 4568, 23 | ) 24 | 25 | RSpec.configure do |config| 26 | config.before(:each, :sqs) { $fake_sqs.start } 27 | config.before(:each, :sqs) { $fake_sqs.reset } 28 | config.after(:suite) { $fake_sqs.stop } 29 | end 30 | -------------------------------------------------------------------------------- /lib/fake_sqs/error_responses.yml: -------------------------------------------------------------------------------- 1 | AccessDenied: 403 2 | AuthFailure: 401 3 | ConflictingQueryParameter: 400 4 | InternalError: 500 5 | InvalidAccessKeyId: 401 6 | InvalidAction: 400 7 | InvalidAddress: 404 8 | InvalidAttributeName: 400 9 | InvalidHttpRequest: 400 10 | InvalidMessageContents: 400 11 | InvalidParameterCombination: 400 12 | InvalidParameterValue: 400 13 | InvalidQueryParameter: 400 14 | InvalidRequest: 400 15 | InvalidSecurity: 403 16 | InvalidSecurityToken: 400 17 | MalformedVersion: 400 18 | MessageTooLong: 400 19 | MessageNotInflight: 400 20 | MissingClientTokenId: 403 21 | MissingCredentials: 401 22 | MissingParameter: 400 23 | NoSuchVersion: 400 24 | NonExistentQueue: 400 25 | NotAuthorizedToUseVersion: 401 26 | QueueDeletedRecently: 400 27 | QueueNameExists: 400 28 | ReadCountOutOfRange: 400 29 | ReceiptHandleIsInvalid: 400 30 | RequestExpired: 400 31 | RequestThrottled: 403 32 | ServiceUnavailable: 503 33 | X509ParseError: 400 34 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/delete_message_batch.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class DeleteMessageBatch 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | queue = @queues.get(name) 13 | receipts = params.select { |k,v| k =~ /DeleteMessageBatchRequestEntry\.\d+\.ReceiptHandle/ } 14 | 15 | deleted = [] 16 | 17 | receipts.each do |key, value| 18 | id = key.split('.')[1] 19 | queue.delete_message(value) 20 | deleted << params.fetch("DeleteMessageBatchRequestEntry.#{id}.Id") 21 | end 22 | 23 | @responder.call :DeleteMessageBatch do |xml| 24 | deleted.each do |id| 25 | xml.DeleteMessageBatchResultEntry do 26 | xml.Id id 27 | end 28 | end 29 | end 30 | end 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 iain 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/unit/collection_view_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/collection_view' 2 | 3 | RSpec.describe FakeSQS::CollectionView do 4 | 5 | def wrap(collection) 6 | FakeSQS::CollectionView.new(collection) 7 | end 8 | 9 | it 'should correctly wrap an array' do 10 | array = %w{one two three four} 11 | view = wrap(array) 12 | expect(view[0]).to eq 'one' 13 | expect(view[1]).to eq 'two' 14 | expect(view[2]).to eq 'three' 15 | expect(view[3]).to eq 'four' 16 | end 17 | 18 | it 'should correctly wrap a hash' do 19 | hash = { :one => 1, :two => 2, :three => 3 } 20 | view = wrap(hash) 21 | expect(view[:one]).to eq 1 22 | expect(view[:two]).to eq 2 23 | expect(view[:three]).to eq 3 24 | end 25 | 26 | it 'should respond to empty correctly' do 27 | expect(wrap([])).to be_empty 28 | expect(wrap({'one' => 1})).to_not be_empty 29 | end 30 | 31 | it 'should be enumerable' do 32 | result = wrap([1, 2, 3]).map { |i| i * i } 33 | expect(result).to eq [1, 4, 9] 34 | end 35 | 36 | it 'should respond to size/length' do 37 | expect(wrap([1, 2, 3]).size).to eq 3 38 | expect(wrap([]).size).to eq 0 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/unit/responder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/responder' 2 | require 'active_support/core_ext/hash' 3 | require 'verbose_hash_fetch' 4 | 5 | RSpec.describe FakeSQS::Responder do 6 | 7 | it "yields xml" do 8 | xml = subject.call :GetQueueUrl do |x| 9 | x.QueueUrl "example.com" 10 | end 11 | 12 | data = Hash.from_xml(xml) 13 | url = data. 14 | fetch("GetQueueUrlResponse"). 15 | fetch("GetQueueUrlResult"). 16 | fetch("QueueUrl") 17 | expect(url).to eq "example.com" 18 | end 19 | 20 | it "skips result if no block is given" do 21 | xml = subject.call :DeleteQueue 22 | 23 | data = Hash.from_xml(xml) 24 | 25 | response = data.fetch("DeleteQueueResponse") 26 | expect(response).to have_key("ResponseMetadata") 27 | expect(response).not_to have_key("DeleteQueueResult") 28 | end 29 | 30 | it "has metadata" do 31 | xml = subject.call :GetQueueUrl do |x| 32 | end 33 | 34 | data = Hash.from_xml(xml) 35 | 36 | request_id = data. 37 | fetch("GetQueueUrlResponse"). 38 | fetch("ResponseMetadata"). 39 | fetch("RequestId") 40 | 41 | expect(request_id.size).to eq 36 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /lib/fake_sqs/web_interface.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | module FakeSQS 3 | class WebInterface < Sinatra::Base 4 | 5 | configure do 6 | use FakeSQS::CatchErrors, response: ErrorResponse 7 | end 8 | 9 | helpers do 10 | 11 | def action 12 | params.fetch("Action") 13 | end 14 | 15 | end 16 | 17 | get "/" do 18 | 200 19 | end 20 | 21 | delete "/" do 22 | settings.api.reset 23 | 200 24 | end 25 | 26 | put "/" do 27 | settings.api.expire 28 | 200 29 | end 30 | 31 | post "/fail" do 32 | settings.api.api_fail(params['action']) 33 | end 34 | 35 | post "/clear_failure" do 36 | settings.api.clear_failure 37 | end 38 | 39 | post "/" do 40 | params['logger'] = logger 41 | if params['QueueUrl'] 42 | queue = URI.parse(params['QueueUrl']).path.gsub(/\//, '') 43 | return settings.api.call(action, queue, params) unless queue.empty? 44 | end 45 | 46 | settings.api.call(action, params) 47 | end 48 | 49 | post "/:queue" do |queue| 50 | settings.api.call(action, queue, params) 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/fake_sqs/error_response.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'builder' 3 | require 'securerandom' 4 | 5 | module FakeSQS 6 | class ErrorResponse 7 | 8 | attr_reader :error 9 | 10 | def initialize(error) 11 | @error = error 12 | end 13 | 14 | def status 15 | @status ||= statuses.fetch(code) 16 | end 17 | 18 | def body 19 | xml = Builder::XmlMarkup.new(:index => 4) 20 | xml.ErrorResponse do 21 | xml.Error do 22 | xml.Type type 23 | xml.Code code 24 | xml.Message error.to_s 25 | xml.Detail 26 | end 27 | xml.RequestId SecureRandom.uuid 28 | end 29 | end 30 | 31 | private 32 | 33 | def code 34 | code = error.class.name.sub(/^FakeSQS::/, '') 35 | if statuses.has_key?(code) 36 | code 37 | else 38 | "InternalError" 39 | end 40 | end 41 | 42 | def type 43 | if status < 500 44 | "Sender" 45 | else 46 | "Receiver" 47 | end 48 | end 49 | 50 | def statuses 51 | @statuses ||= YAML.load_file(File.expand_path('../error_responses.yml', __FILE__)) 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/fake_sqs/actions/send_message_batch.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | module Actions 3 | class SendMessageBatch 4 | 5 | def initialize(options = {}) 6 | @server = options.fetch(:server) 7 | @queues = options.fetch(:queues) 8 | @responder = options.fetch(:responder) 9 | end 10 | 11 | def call(name, params) 12 | queue = @queues.get(name) 13 | 14 | messages = params.select { |k,v| k =~ /SendMessageBatchRequestEntry\.\d+\.MessageBody/ } 15 | 16 | results = {} 17 | 18 | messages.each do |key, value| 19 | id = key.split('.')[1] 20 | msg_id = params.fetch("SendMessageBatchRequestEntry.#{id}.Id") 21 | delay = params["SendMessageBatchRequestEntry.#{id}.DelaySeconds"] 22 | message = queue.send_message("MessageBody" => value, "DelaySeconds" => delay) 23 | results[msg_id] = message 24 | end 25 | 26 | @responder.call :SendMessageBatch do |xml| 27 | results.each do |msg_id, message| 28 | xml.SendMessageBatchResultEntry do 29 | xml.Id msg_id 30 | xml.MessageId message.id 31 | xml.MD5OfMessageBody message.md5 32 | end 33 | end 34 | end 35 | end 36 | 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /fake_sqs.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'fake_sqs/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "fake_sqs" 8 | gem.version = FakeSQS::VERSION 9 | gem.authors = ["iain"] 10 | gem.email = ["iain@iain.nl"] 11 | gem.description = %q{Provides a fake SQS server that you can run locally to test against} 12 | gem.summary = %q{Provides a fake SQS server that you can run locally to test against} 13 | gem.homepage = "https://github.com/iain/fake_sqs" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | gem.license = "MIT" 20 | 21 | gem.add_dependency "sinatra" 22 | gem.add_dependency "builder" 23 | 24 | gem.add_development_dependency "rspec" 25 | gem.add_development_dependency "rake" 26 | gem.add_development_dependency "aws-sdk", "~> 2.0" 27 | gem.add_development_dependency "faraday" 28 | gem.add_development_dependency "thin" 29 | gem.add_development_dependency "verbose_hash_fetch" 30 | gem.add_development_dependency "activesupport" 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/catch_errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/catch_errors' 2 | 3 | RSpec.describe FakeSQS::CatchErrors do 4 | 5 | let(:app) { double :app } 6 | let(:error_response) { double :error_response, :status => 500, :body => "X" } 7 | let(:response) { double :response, :new => error_response } 8 | subject(:catch_errors) { FakeSQS::CatchErrors.new(app, response: response) } 9 | 10 | context "when the app behaves normally" do 11 | 12 | let(:normal_response) { double :normal_response } 13 | before { allow(app).to receive(:call).and_return(normal_response) } 14 | 15 | it "doesn't modify normal responses" do 16 | expect(catch_errors.call({})).to eq normal_response 17 | end 18 | 19 | end 20 | 21 | context "when the app raises an exception" do 22 | 23 | let(:error) { RuntimeError.new("it went wrong") } 24 | before { allow(app).to receive(:call).and_raise(error) } 25 | 26 | it "cathes and processes errors" do 27 | expect(response).to receive(:new).with(error) 28 | catch_errors.call({}) 29 | end 30 | 31 | it "sets the status determined by the error response" do 32 | allow(error_response).to receive(:status).and_return(123) 33 | expect(catch_errors.call({}).fetch(0)).to eq 123 34 | end 35 | 36 | it "set the body determined by the error response" do 37 | allow(error_response).to receive(:body).and_return("foobar") 38 | expect(catch_errors.call({}).fetch(2)).to eq ["foobar"] 39 | end 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/message_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/message' 2 | 3 | RSpec.describe FakeSQS::Message do 4 | 5 | describe "#body" do 6 | 7 | it "is extracted from the MessageBody" do 8 | message = create_message("MessageBody" => "abc") 9 | expect(message.body).to eq "abc" 10 | end 11 | 12 | end 13 | 14 | describe "#md5" do 15 | 16 | it "is calculated from body" do 17 | message = create_message("MessageBody" => "abc") 18 | expect(message.md5).to eq "900150983cd24fb0d6963f7d28e17f72" 19 | end 20 | 21 | end 22 | 23 | describe "#id" do 24 | 25 | it "is generated" do 26 | message = create_message 27 | expect(message.id.size).to eq 36 28 | end 29 | 30 | end 31 | 32 | describe 'visibility_timeout' do 33 | 34 | let :message do 35 | create_message 36 | end 37 | 38 | it 'should default to nil' do 39 | expect(message.visibility_timeout).to eq nil 40 | end 41 | 42 | it 'should be expired when it is nil' do 43 | expect(message).to be_expired 44 | end 45 | 46 | it 'should be expired if set to a previous time' do 47 | message.visibility_timeout = Time.now - 1 48 | expect(message).to be_expired 49 | end 50 | 51 | it 'should not be expired at a future date' do 52 | message.visibility_timeout = Time.now + 1 53 | expect(message).not_to be_expired 54 | end 55 | 56 | it 'should not be expired when set to expire at a future date' do 57 | message.expire_at(5) 58 | expect(message.visibility_timeout).to be >=(Time.now + 4) 59 | end 60 | 61 | end 62 | 63 | def create_message(options = {}) 64 | FakeSQS::Message.new({"MessageBody" => "test"}.merge(options)) 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /lib/fake_sqs/file_database.rb: -------------------------------------------------------------------------------- 1 | require "yaml/store" 2 | 3 | module FakeSQS 4 | class FileDatabase 5 | 6 | attr_reader :filename 7 | 8 | def initialize(filename) 9 | @filename = filename 10 | @queue_objects = {} 11 | end 12 | 13 | def load 14 | transaction do 15 | store["queues"] ||= {} 16 | end 17 | end 18 | 19 | def transaction 20 | store.transaction do 21 | yield 22 | end 23 | end 24 | 25 | def reset 26 | transaction do 27 | store["queues"] = {} 28 | end 29 | @queue_objects = {} 30 | end 31 | 32 | def []=(key, value) 33 | storage[key] = value.to_yaml 34 | end 35 | 36 | def [](key) 37 | value = storage[key] 38 | if value 39 | deserialize(key) 40 | else 41 | value 42 | end 43 | end 44 | 45 | def each(&block) 46 | storage.each do |key, value| 47 | yield key, deserialize(key) 48 | end 49 | end 50 | 51 | def select(&block) 52 | new_list = storage.select do |key, value| 53 | yield key, deserialize(key) 54 | end 55 | Hash[new_list.map { |key, value| [key, deserialize(key)] }] 56 | end 57 | 58 | def delete(key) 59 | storage.delete(key) 60 | end 61 | 62 | def values 63 | storage.map { |key, value| 64 | deserialize(key) 65 | } 66 | end 67 | 68 | private 69 | 70 | def deserialize(key) 71 | @queue_objects[key] ||= Queue.new(storage[key].merge(message_factory: Message)) 72 | end 73 | 74 | def storage 75 | store["queues"] 76 | end 77 | 78 | def store 79 | @store ||= YAML::Store.new(filename) 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/fake_sqs/queues.rb: -------------------------------------------------------------------------------- 1 | module FakeSQS 2 | 3 | QueueNameExists = Class.new(RuntimeError) 4 | NonExistentQueue = Class.new(RuntimeError) 5 | 6 | class Queues 7 | 8 | attr_reader :queue_factory, :database 9 | 10 | def initialize(options = {}) 11 | @queue_factory = options.fetch(:queue_factory) 12 | @database = options.fetch(:database) 13 | @database.load 14 | end 15 | 16 | def create(name, options = {}) 17 | return database[name] if database[name] 18 | queue = queue_factory.new(options) 19 | database[name] = queue 20 | end 21 | 22 | def delete(name, options = {}) 23 | if database[name] 24 | database.delete(name) 25 | else 26 | fail NonExistentQueue, name 27 | end 28 | end 29 | 30 | def list(options = {}) 31 | if (prefix = options["QueueNamePrefix"]) 32 | database.select { |name, queue| name.start_with?(prefix) }.values 33 | else 34 | database.values 35 | end 36 | end 37 | 38 | def get(name, options = {}) 39 | if (db = database[name]) 40 | db 41 | else 42 | fail NonExistentQueue, name 43 | end 44 | end 45 | 46 | def transaction 47 | database.transaction do 48 | yield 49 | end 50 | end 51 | 52 | def save(queue) 53 | database[queue.name] = queue 54 | end 55 | 56 | def reset 57 | database.reset 58 | end 59 | 60 | def timeout_messages! 61 | transaction do 62 | database.each { |name,queue| queue.timeout_messages! } 63 | end 64 | end 65 | 66 | def expire 67 | transaction do 68 | database.each { |name, queue| queue.expire } 69 | end 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /bin/fake_sqs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path("../../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | require 'fake_sqs' 7 | require 'optparse' 8 | 9 | options = { 10 | :port => 4568, 11 | :host => "0.0.0.0", 12 | :verbose => false, 13 | :daemonize => false, 14 | :database => ":memory:" 15 | } 16 | 17 | parser = OptionParser.new do |o| 18 | 19 | o.on "--database DATABASE", "Where to store the database (default: #{options[:database]})" do |database| 20 | options[:database] = database 21 | end 22 | 23 | o.on "-p", "--port PORT", Integer, "Port to use (default: #{options[:port]})" do |port| 24 | options[:port] = port 25 | end 26 | 27 | o.on "-o", "--bind HOST", "Host to bind to (default: #{options[:host]})" do |host| 28 | options[:host] = host 29 | end 30 | 31 | o.on "-s", "--server SERVER", ['thin', 'mongrel', 'webrick'], "Server to use: thin, mongrel or webrick (by default Sinatra chooses the best available)" do |server| 32 | options[:server] = server 33 | end 34 | 35 | o.on "-P", "--pid PIDFILE", "Where to write the pid" do |pid| 36 | options[:pid] = pid 37 | end 38 | 39 | o.on "-d", "--[no-]daemonize", "Detaches the process" do |daemonize| 40 | options[:daemonize] = daemonize 41 | end 42 | 43 | o.on "-v", "--[no-]verbose", "Shows input parameters and output XML" do |verbose| 44 | options[:verbose] = verbose 45 | end 46 | 47 | o.on "--log FILE", "Redirect output to this logfile (default: console)" do |logfile| 48 | options[:log] = logfile 49 | end 50 | 51 | o.on_tail "--version", "Shows the version" do 52 | puts "fake_sqs version #{FakeSQS::VERSION}" 53 | exit 54 | end 55 | 56 | o.on_tail "-h", "--help", "Shows this help page" do 57 | puts o 58 | exit 59 | end 60 | 61 | end 62 | 63 | parser.parse! 64 | 65 | FakeSQS.to_rack(options).run! 66 | -------------------------------------------------------------------------------- /lib/fake_sqs/api.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/actions/change_message_visibility' 2 | require 'fake_sqs/actions/create_queue' 3 | require 'fake_sqs/actions/delete_queue' 4 | require 'fake_sqs/actions/list_queues' 5 | require 'fake_sqs/actions/get_queue_url' 6 | require 'fake_sqs/actions/send_message' 7 | require 'fake_sqs/actions/receive_message' 8 | require 'fake_sqs/actions/delete_message' 9 | require 'fake_sqs/actions/delete_message_batch' 10 | require 'fake_sqs/actions/purge_queue' 11 | require 'fake_sqs/actions/send_message_batch' 12 | require 'fake_sqs/actions/get_queue_attributes' 13 | require 'fake_sqs/actions/set_queue_attributes' 14 | 15 | module FakeSQS 16 | 17 | InvalidAction = Class.new(ArgumentError) 18 | 19 | class API 20 | 21 | attr_reader :queues, :options 22 | 23 | def initialize(options = {}) 24 | @queues = options.fetch(:queues) 25 | @options = options 26 | @run_timer = true 27 | @timer = Thread.new do 28 | while @run_timer 29 | queues.timeout_messages! 30 | sleep(5) 31 | end 32 | end 33 | end 34 | 35 | def call(action, *args) 36 | if FakeSQS::Actions.const_defined?(action) 37 | action = FakeSQS::Actions.const_get(action).new(options) 38 | queues.transaction do 39 | action.call(*args) 40 | end 41 | else 42 | fail InvalidAction, "Unknown (or not yet implemented) action: #{action}" 43 | end 44 | end 45 | 46 | # Fake actions 47 | 48 | def reset 49 | queues.reset 50 | end 51 | 52 | def expire 53 | queues.expire 54 | end 55 | 56 | def stop 57 | @run_timer = false 58 | end 59 | 60 | # Allow for simulating queue action failures 61 | def api_fail(action) 62 | queues.list.each { |q| q.fail_actions.push action.to_sym } 63 | end 64 | 65 | # Clear existing simulation queue failures 66 | def clear_failure 67 | queues.list.map {|q| q.fail_actions.clear} 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/unit/api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/api' 2 | 3 | class FakeSQS::Actions::TheAction 4 | 5 | def initialize(options) 6 | @options = options 7 | end 8 | 9 | def call(params) 10 | { :options => @options, :params => params } 11 | end 12 | 13 | end 14 | 15 | 16 | RSpec.describe FakeSQS::API do 17 | 18 | it "delegates actions to classes" do 19 | allow(@queues).to receive(:transaction).and_yield 20 | api = FakeSQS::API.new(:queues => @queues) 21 | response = api.call("TheAction", {:foo => "bar"}) 22 | 23 | expect(response[:options]).to eq :queues => queues 24 | expect(response[:params]).to eq :foo => "bar" 25 | end 26 | 27 | it "raises InvalidAction for unknown actions" do 28 | api = FakeSQS::API.new(:queues => []) 29 | expect { 30 | api.call("SomethingDifferentAndUnknown", {:foo => "bar"}) 31 | }.to raise_error(FakeSQS::InvalidAction) 32 | end 33 | 34 | it "resets queues" do 35 | queues = double :queues 36 | api = FakeSQS::API.new(:queues => queues) 37 | expect(queues).to receive(:reset) 38 | api.reset 39 | end 40 | 41 | it "expires messages in queues" do 42 | queues = double :queues 43 | api = FakeSQS::API.new(:queues => queues) 44 | expect(queues).to receive(:expire) 45 | api.expire 46 | end 47 | 48 | context 'simulates failure' do 49 | before do 50 | @queues.stub(:list).and_return([@queue1=FakeSQS::Queue.new("QueueName" => 'default', :message_factory => MessageFactory.new)]) 51 | @api = FakeSQS::API.new(:queues => @queues) 52 | @api.api_fail(:send_message) 53 | @api.api_fail(:receive_message) 54 | end 55 | 56 | it "fails on sending message" do 57 | expect { @queue1.send_message }.to raise_error FakeSQS::InvalidAction 58 | end 59 | 60 | it "fails on receiving message" do 61 | expect { @queue1.receive_message }.to raise_error FakeSQS::InvalidAction 62 | end 63 | 64 | it "resets failures after setting" do 65 | @api.clear_failure 66 | expect { @queue1.send_message }.to_not raise_error 67 | expect { @queue1.receive_message }.to_not raise_error 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /spec/unit/error_response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/error_response' 2 | require 'active_support/core_ext/hash' 3 | require 'verbose_hash_fetch' 4 | 5 | RSpec.describe FakeSQS::ErrorResponse do 6 | 7 | module FakeSQS 8 | MissingCredentials = Class.new(RuntimeError) 9 | end 10 | ErrorUnknownToSQS = Class.new(RuntimeError) 11 | 12 | describe "#status" do 13 | 14 | it "picks the right error status" do 15 | error = FakeSQS::MissingCredentials.new("message") 16 | response = FakeSQS::ErrorResponse.new(error) 17 | expect(response.status).to eq 401 18 | end 19 | 20 | it "uses 400 as default status" do 21 | error = ErrorUnknownToSQS.new("message") 22 | response = FakeSQS::ErrorResponse.new(error) 23 | expect(response.status).to eq 500 24 | end 25 | 26 | end 27 | 28 | describe "#body" do 29 | 30 | let(:error) { FakeSQS::MissingCredentials.new("the message") } 31 | let(:response) { FakeSQS::ErrorResponse.new(error) } 32 | let(:data) { Hash.from_xml(response.body) } 33 | 34 | it "uses the error class name as error code" do 35 | expect(data.fetch("ErrorResponse").fetch("Error").fetch("Code")).to eq "MissingCredentials" 36 | end 37 | 38 | it "uses InternalError as code for unknown errors" do 39 | error = ErrorUnknownToSQS.new("the message") 40 | response = FakeSQS::ErrorResponse.new(error) 41 | data = Hash.from_xml(response.body) 42 | expect(data.fetch("ErrorResponse").fetch("Error").fetch("Code")).to eq "InternalError" 43 | end 44 | 45 | it "uses the to_s of the error as message" do 46 | expect(data.fetch("ErrorResponse").fetch("Error").fetch("Message")).to eq "the message" 47 | end 48 | 49 | it "has a request id" do 50 | expect(data.fetch("ErrorResponse").fetch("RequestId").size).to eq 36 51 | end 52 | 53 | it "uses Sender as type for 4xx responses" do 54 | allow(response).to receive(:status).and_return(400) 55 | expect(data.fetch("ErrorResponse").fetch("Error").fetch("Type")).to eq "Sender" 56 | end 57 | 58 | it "uses Receiver as type for 5xx responses" do 59 | allow(response).to receive(:status).and_return(500) 60 | expect(data.fetch("ErrorResponse").fetch("Error").fetch("Type")).to eq "Receiver" 61 | end 62 | 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/acceptance/queue_actions_spec.rb: -------------------------------------------------------------------------------- 1 | require "integration_spec_helper" 2 | 3 | RSpec.describe "Actions for Queues", :sqs do 4 | 5 | let(:sqs) { Aws::SQS::Client.new } 6 | before do 7 | sqs.config.endpoint = $fake_sqs.uri 8 | end 9 | 10 | specify "CreateQueue" do 11 | response = sqs.create_queue(queue_name: "test-create-queue") 12 | expect(response.queue_url).to eq "http://0.0.0.0:4568/test-create-queue" 13 | response2 = sqs.get_queue_attributes(queue_url: response.queue_url) 14 | expect(response2.attributes.fetch("QueueArn")).to match %r"arn:aws:sqs:us-east-1:.+:test-create-queue" 15 | end 16 | 17 | specify "GetQueueUrl" do 18 | sqs.create_queue(queue_name: "test-get-queue-url") 19 | response = sqs.get_queue_url(queue_name: "test-get-queue-url") 20 | expect(response.queue_url).to eq "http://0.0.0.0:4568/test-get-queue-url" 21 | end 22 | 23 | specify "ListQueues" do 24 | sqs.create_queue(queue_name: "test-list-1") 25 | sqs.create_queue(queue_name: "test-list-2") 26 | expect(sqs.list_queues.queue_urls).to eq [ 27 | "http://0.0.0.0:4568/test-list-1", 28 | "http://0.0.0.0:4568/test-list-2" 29 | ] 30 | end 31 | 32 | specify "ListQueues with prefix" do 33 | sqs.create_queue(queue_name: "test-list-1") 34 | sqs.create_queue(queue_name: "test-list-2") 35 | sqs.create_queue(queue_name: "other-list-3") 36 | expect(sqs.list_queues(queue_name_prefix: "test").queue_urls).to eq [ 37 | "http://0.0.0.0:4568/test-list-1", 38 | "http://0.0.0.0:4568/test-list-2", 39 | ] 40 | end 41 | 42 | specify "DeleteQueue" do 43 | url = sqs.create_queue(queue_name: "test-delete").queue_url 44 | expect(sqs.list_queues.queue_urls.size).to eq 1 45 | sqs.delete_queue(queue_url: url) 46 | expect(sqs.list_queues.queue_urls.size).to eq 0 47 | end 48 | 49 | specify "SetQueueAttributes / GetQueueAttributes" do 50 | queue_url = sqs.create_queue(queue_name: "my-queue").queue_url 51 | 52 | 53 | sqs.set_queue_attributes( 54 | queue_url: queue_url, 55 | attributes: { 56 | "DelaySeconds" => "900" 57 | } 58 | ) 59 | 60 | response = sqs.get_queue_attributes( 61 | queue_url: queue_url, 62 | ) 63 | expect(response.attributes.fetch("DelaySeconds")).to eq "900" 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/fake_sqs.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/api' 2 | require 'fake_sqs/catch_errors' 3 | require 'fake_sqs/collection_view' 4 | require 'fake_sqs/error_response' 5 | require 'fake_sqs/message' 6 | require 'fake_sqs/queue' 7 | require 'fake_sqs/queue_factory' 8 | require 'fake_sqs/queues' 9 | require 'fake_sqs/responder' 10 | require 'fake_sqs/server' 11 | require 'fake_sqs/version' 12 | require 'fake_sqs/memory_database' 13 | require 'fake_sqs/file_database' 14 | 15 | module FakeSQS 16 | 17 | def self.to_rack(options) 18 | 19 | require 'fake_sqs/web_interface' 20 | app = FakeSQS::WebInterface 21 | 22 | if (log = options[:log]) 23 | file = File.new(log, "a+") 24 | file.sync = true 25 | app.use Rack::CommonLogger, file 26 | app.set :log_file, file 27 | app.enable :logging 28 | end 29 | 30 | if options[:verbose] 31 | require 'fake_sqs/show_output' 32 | app.use FakeSQS::ShowOutput 33 | app.enable :logging 34 | end 35 | 36 | if options[:daemonize] 37 | require 'fake_sqs/daemonize' 38 | Daemonize.new(options).call 39 | end 40 | 41 | app.set :port, options[:port] if options[:port] 42 | app.set :bind, options[:host] if options[:host] 43 | app.set :server, options[:server] if options[:server] 44 | server = FakeSQS.server(port: options[:port], host: options[:host]) 45 | app.set :api, FakeSQS.api(server: server, database: options[:database]) 46 | app 47 | end 48 | 49 | def self.server(options = {}) 50 | Server.new(options) 51 | end 52 | 53 | def self.api(options = {}) 54 | db = database_for(options.fetch(:database) { ":memory:" }) 55 | API.new( 56 | server: options.fetch(:server), 57 | queues: queues(db), 58 | responder: responder 59 | ) 60 | end 61 | 62 | def self.queues(database) 63 | Queues.new(queue_factory: queue_factory, database: database) 64 | end 65 | 66 | def self.responder 67 | Responder.new 68 | end 69 | 70 | def self.queue_factory 71 | QueueFactory.new(message_factory: message_factory, queue: queue) 72 | end 73 | 74 | def self.message_factory 75 | Message 76 | end 77 | 78 | def self.queue 79 | Queue 80 | end 81 | 82 | def self.database_for(name) 83 | if name == ":memory:" 84 | MemoryDatabase.new 85 | else 86 | FileDatabase.new(name) 87 | end 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /lib/fake_sqs/test_integration.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | 3 | module FakeSQS 4 | class TestIntegration 5 | 6 | attr_reader :options 7 | 8 | def initialize(options = {}) 9 | @options = options 10 | end 11 | 12 | def host 13 | option :sqs_endpoint 14 | end 15 | 16 | def port 17 | option :sqs_port 18 | end 19 | 20 | def start 21 | start! unless up? 22 | reset 23 | end 24 | 25 | def start! 26 | args = [ binfile, "-p", port.to_s, verbose, logging, database, { :out => out, :err => out } ].flatten.compact 27 | @pid = Process.spawn(*args) 28 | wait_until_up 29 | end 30 | 31 | def stop 32 | if @pid 33 | Process.kill("INT", @pid) 34 | Process.waitpid(@pid) 35 | @pid = nil 36 | else 37 | $stderr.puts "FakeSQS is not running" 38 | end 39 | end 40 | 41 | def reset 42 | connection.delete("/") 43 | end 44 | 45 | def expire 46 | connection.put("/", "") 47 | end 48 | 49 | def api_fail(action) 50 | connection.post("/fail", "action=#{action.to_s}") 51 | end 52 | 53 | def clear_failure 54 | connection.post("/clear_failure", "") 55 | end 56 | 57 | def url 58 | "http://#{host}:#{port}" 59 | end 60 | 61 | def uri 62 | URI(url) 63 | end 64 | 65 | def up? 66 | @pid && connection.get("/").code.to_s == "200" 67 | rescue Errno::ECONNREFUSED 68 | false 69 | end 70 | 71 | private 72 | 73 | def option(key) 74 | options.fetch(key, nil) 75 | end 76 | 77 | def database 78 | if db = option(:database) 79 | [ "--database", db ] 80 | else 81 | [] 82 | end 83 | end 84 | 85 | def verbose 86 | if debug? 87 | "--verbose" 88 | else 89 | "--no-verbose" 90 | end 91 | end 92 | 93 | def logging 94 | if (file = ENV["SQS_LOG"] || option(:log)) 95 | [ "--log", file ] 96 | else 97 | [] 98 | end 99 | end 100 | 101 | def wait_until_up(deadline = Time.now + 2) 102 | fail "FakeSQS didn't start in time" if Time.now > deadline 103 | unless up? 104 | sleep 0.01 105 | wait_until_up(deadline) 106 | end 107 | end 108 | 109 | def binfile 110 | File.expand_path("../../../bin/fake_sqs", __FILE__) 111 | end 112 | 113 | def out 114 | if debug? 115 | :out 116 | else 117 | "/dev/null" 118 | end 119 | end 120 | 121 | def connection 122 | @connection ||= Net::HTTP.new(host, port) 123 | end 124 | 125 | def debug? 126 | ENV["DEBUG"].to_s == "true" || option(:debug) 127 | end 128 | 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/unit/queues_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/queues' 2 | require 'fake_sqs/memory_database' 3 | 4 | RSpec.describe FakeSQS::Queues do 5 | 6 | let(:fake_database) { FakeSQS::MemoryDatabase.new } 7 | let(:queue_factory) { double :queue_factory, :new => double } 8 | subject(:queues) { FakeSQS::Queues.new(queue_factory: queue_factory, database: fake_database) } 9 | 10 | describe "#create" do 11 | 12 | it "creates new queues" do 13 | expect(queues.list.size).to eq 0 14 | create_queue("test") 15 | expect(queues.list.size).to eq 1 16 | end 17 | 18 | it "uses the queue factory" do 19 | params = double :params 20 | expect(queue_factory).to receive(:new).with(params) 21 | create_queue("test", params) 22 | end 23 | 24 | it "returns the queue" do 25 | queue = double 26 | allow(queue_factory).to receive(:new).and_return(queue) 27 | expect(create_queue("test")).to eq queue 28 | end 29 | 30 | it "returns existing queue if the queue exists" do 31 | queue = create_queue("test") 32 | expect(create_queue("test")).to eq(queue) 33 | end 34 | 35 | end 36 | 37 | describe "#delete" do 38 | 39 | it "deletes an existing queue" do 40 | create_queue("test") 41 | expect(queues.list.size).to eq 1 42 | queues.delete("test") 43 | expect(queues.list.size).to eq 0 44 | end 45 | 46 | it "cannot delete an non-existing queue" do 47 | expect { 48 | queues.delete("test") 49 | }.to raise_error(FakeSQS::NonExistentQueue, "test") 50 | end 51 | 52 | end 53 | 54 | describe "#list" do 55 | 56 | it "returns all the queues" do 57 | queue1 = create_queue("test-1") 58 | queue2 = create_queue("test-2") 59 | expect(queues.list).to eq [ queue1, queue2 ] 60 | end 61 | 62 | it "can be filtered by prefix" do 63 | queue1 = create_queue("test-1") 64 | queue2 = create_queue("test-2") 65 | _ = create_queue("other-3") 66 | expect(queues.list("QueueNamePrefix" => "test")).to eq [ queue1, queue2 ] 67 | end 68 | 69 | end 70 | 71 | describe "#get" do 72 | 73 | it "finds the queue by name" do 74 | queue = create_queue("test") 75 | expect(queues.get("test")).to eq queue 76 | end 77 | 78 | it "cannot get the queue if it doesn't exist" do 79 | expect { 80 | queues.get("test") 81 | }.to raise_error(FakeSQS::NonExistentQueue, "test") 82 | end 83 | 84 | end 85 | 86 | describe "#reset" do 87 | 88 | it "clears all queues" do 89 | create_queue("foo") 90 | create_queue("bar") 91 | expect(queues.list.size).to eq 2 92 | queues.reset 93 | expect(queues.list.size).to eq 0 94 | end 95 | 96 | end 97 | 98 | def create_queue(name, options = {}) 99 | queues.create(name, options) 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/fake_sqs/queue.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'fake_sqs/collection_view' 3 | 4 | module FakeSQS 5 | 6 | MessageNotInflight = Class.new(RuntimeError) 7 | ReadCountOutOfRange = Class.new(RuntimeError) 8 | ReceiptHandleIsInvalid = Class.new(RuntimeError) 9 | InvalidAction = Class.new(RuntimeError) 10 | 11 | class Queue 12 | 13 | VISIBILITY_TIMEOUT = 30 14 | 15 | attr_reader :name, :message_factory, :arn, :queue_attributes, :fail_actions 16 | 17 | def initialize(options = {}) 18 | @message_factory = options.fetch(:message_factory) 19 | 20 | @name = options.fetch("QueueName") 21 | @arn = options.fetch("Arn") { "arn:aws:sqs:us-east-1:#{SecureRandom.hex}:#{@name}" } 22 | @queue_attributes = options.fetch("Attributes") { {} } 23 | @lock = Monitor.new 24 | @fail_actions = [] 25 | reset 26 | end 27 | 28 | def to_yaml 29 | { 30 | "QueueName" => name, 31 | "Arn" => arn, 32 | "Attributes" => queue_attributes, 33 | } 34 | end 35 | 36 | def add_queue_attributes(attrs) 37 | queue_attributes.merge!(attrs) 38 | end 39 | 40 | def attributes 41 | queue_attributes.merge( 42 | "QueueArn" => arn, 43 | "ApproximateNumberOfMessages" => @messages.size, 44 | "ApproximateNumberOfMessagesNotVisible" => @messages_in_flight.size, 45 | ) 46 | end 47 | 48 | def send_message(options = {}) 49 | fail InvalidAction if @fail_actions.include?(:send_message) 50 | with_lock do 51 | message = message_factory.new(options) 52 | @messages << message 53 | message 54 | end 55 | end 56 | 57 | def receive_message(options = {}) 58 | 59 | fail InvalidAction if @fail_actions.include?(:receive_message) 60 | 61 | amount = Integer options.fetch("MaxNumberOfMessages") { "1" } 62 | 63 | fail ReadCountOutOfRange, amount if amount > 10 64 | 65 | return {} if @messages.empty? 66 | 67 | result = {} 68 | 69 | with_lock do 70 | actual_amount = amount > size ? size : amount 71 | 72 | actual_amount.times do 73 | message = @messages.delete_at(rand(size)) 74 | message.expire_at(default_visibility_timeout) 75 | receipt = generate_receipt 76 | @messages_in_flight[receipt] = message 77 | result[receipt] = message 78 | end 79 | end 80 | 81 | result 82 | end 83 | 84 | def default_visibility_timeout 85 | if value = attributes['VisibilityTimeout'] 86 | value.to_i 87 | else 88 | VISIBILITY_TIMEOUT 89 | end 90 | end 91 | 92 | def timeout_messages! 93 | with_lock do 94 | expired = @messages_in_flight.inject({}) do |memo,(receipt,message)| 95 | if message.expired? 96 | memo[receipt] = message 97 | end 98 | memo 99 | end 100 | expired.each do |receipt,message| 101 | message.expire! 102 | @messages << message 103 | delete_message(receipt) 104 | end 105 | end 106 | end 107 | 108 | def change_message_visibility(receipt, visibility) 109 | with_lock do 110 | message = @messages_in_flight[receipt] 111 | raise MessageNotInflight unless message 112 | 113 | if visibility == 0 114 | message.expire! 115 | @messages << message 116 | delete_message(receipt) 117 | else 118 | message.expire_at(visibility) 119 | end 120 | 121 | end 122 | end 123 | 124 | def delete_message(receipt) 125 | with_lock do 126 | @messages_in_flight.delete(receipt) 127 | end 128 | end 129 | 130 | def reset 131 | with_lock do 132 | @messages = [] 133 | @messages_view = FakeSQS::CollectionView.new(@messages) 134 | reset_messages_in_flight 135 | end 136 | end 137 | 138 | def expire 139 | with_lock do 140 | @messages += @messages_in_flight.values 141 | reset_messages_in_flight 142 | end 143 | end 144 | 145 | def reset_messages_in_flight 146 | with_lock do 147 | @messages_in_flight = {} 148 | @messages_in_flight_view = FakeSQS::CollectionView.new(@messages_in_flight) 149 | end 150 | end 151 | 152 | def messages 153 | @messages_view 154 | end 155 | 156 | def messages_in_flight 157 | @messages_in_flight_view 158 | end 159 | 160 | def size 161 | messages.size 162 | end 163 | 164 | def generate_receipt 165 | SecureRandom.hex 166 | end 167 | 168 | def with_lock 169 | @lock.synchronize do 170 | yield 171 | end 172 | end 173 | 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/unit/queue_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fake_sqs/queue' 2 | require 'fake_sqs/message' 3 | 4 | RSpec.describe FakeSQS::Queue do 5 | 6 | class MessageFactory 7 | def new(options = {}) 8 | FakeSQS::Message.new({'MessageBody' => 'sample-body'}.merge(options)) 9 | end 10 | end 11 | 12 | let(:message_factory) { MessageFactory.new } 13 | subject(:queue) { FakeSQS::Queue.new(:message_factory => message_factory, "QueueName" => "test-queue") } 14 | 15 | describe "#send_message" do 16 | 17 | it "adds a message" do 18 | expect(queue.messages.size).to eq 0 19 | send_message 20 | expect(queue.messages.size).to eq 1 21 | end 22 | 23 | it "returns the message" do 24 | message = double.as_null_object 25 | allow(message_factory).to receive(:new).and_return(message) 26 | expect(send_message).to eq message 27 | end 28 | 29 | it "uses the message factory" do 30 | options = { "MessageBody" => "abc" } 31 | expect(message_factory).to receive(:new).with(options) 32 | send_message(options) 33 | end 34 | 35 | end 36 | 37 | describe "#receive_message" do 38 | 39 | it "gets the message" do 40 | sent = send_message 41 | received = receive_message 42 | expect(received.values.first).to eq sent 43 | end 44 | 45 | it "gets you a random message" do 46 | indexes = { :first => 0, :second => 0 } 47 | sample_group = 1_000 48 | half_sample_group = sample_group / 2 49 | ten_percent = half_sample_group / 0.1 50 | 51 | sample_group.times do 52 | sent_first = send_message 53 | _ = send_message 54 | message = receive_message.values.first 55 | if message == sent_first 56 | indexes[:first] += 1 57 | else 58 | indexes[:second] += 1 59 | end 60 | reset_queue 61 | end 62 | 63 | expect(indexes[:first] + indexes[:second]).to eq sample_group 64 | 65 | expect(indexes[:first]).to be_within(ten_percent).of(half_sample_group) 66 | expect(indexes[:second]).to be_within(ten_percent).of(half_sample_group) 67 | end 68 | 69 | it "cannot get received messages" do 70 | sample_group = 1_000 71 | 72 | sample_group.times do 73 | sent_first = send_message 74 | sent_second = send_message 75 | received_first = receive_message.values.first 76 | 77 | if received_first == sent_first 78 | expect(receive_message.values.first).to eq sent_second 79 | else 80 | expect(receive_message.values.first).to eq sent_first 81 | end 82 | reset_queue 83 | end 84 | end 85 | 86 | it "keeps track of sent messages" do 87 | 88 | send_message 89 | 90 | expect(queue.messages_in_flight.size).to eq 0 91 | expect(queue.attributes["ApproximateNumberOfMessagesNotVisible"]).to eq 0 92 | expect(queue.attributes["ApproximateNumberOfMessages"]).to eq 1 93 | 94 | receive_message 95 | 96 | expect(queue.messages_in_flight.size).to eq 1 97 | expect(queue.attributes["ApproximateNumberOfMessagesNotVisible"]).to eq 1 98 | expect(queue.attributes["ApproximateNumberOfMessages"]).to eq 0 99 | end 100 | 101 | it "gets multiple message" do 102 | sent_first = send_message 103 | sent_second = send_message 104 | messages = receive_message("MaxNumberOfMessages" => "2") 105 | expect(messages.size).to eq 2 106 | expect(messages.values).to match_array [ sent_first, sent_second ] 107 | end 108 | 109 | it "won't accept more than 10 message" do 110 | expect { 111 | receive_message("MaxNumberOfMessages" => "11") 112 | }.to raise_error(FakeSQS::ReadCountOutOfRange, "11") 113 | end 114 | 115 | it "won't error on empty queues" do 116 | expect(receive_message).to eq({}) 117 | end 118 | 119 | end 120 | 121 | describe "#delete_message" do 122 | 123 | it "deletes by the receipt" do 124 | send_message 125 | receipt = receive_message.keys.first 126 | 127 | expect(queue.messages_in_flight.size).to eq 1 128 | queue.delete_message(receipt) 129 | expect(queue.messages_in_flight.size).to eq 0 130 | expect(queue.messages.size).to eq 0 131 | end 132 | 133 | it "won't raise if the receipt is unknown" do 134 | queue.delete_message("abc") 135 | end 136 | 137 | end 138 | 139 | describe "#add_queue_attributes" do 140 | 141 | it "adds to it's queue attributes" do 142 | queue.add_queue_attributes("foo" => "bar") 143 | expect(queue.attributes).to eq( 144 | "foo" => "bar", 145 | "QueueArn" => queue.arn, 146 | "ApproximateNumberOfMessages" => 0, 147 | "ApproximateNumberOfMessagesNotVisible" => 0 148 | ) 149 | end 150 | 151 | end 152 | 153 | def send_message(options = {}) 154 | queue.send_message(options) 155 | end 156 | 157 | def receive_message(options = {}) 158 | queue.receive_message(options) 159 | end 160 | 161 | def reset_queue 162 | queue.reset 163 | end 164 | 165 | end 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fake SQS [![Build Status](https://secure.travis-ci.org/iain/fake_sqs.png)](http://travis-ci.org/iain/fake_sqs) 2 | 3 | Inspired by [Fake DynamoDB] [fake_dynamo], this is an AWS SQS compatible 4 | message queue that can be ran locally. This makes it ideal for integration 5 | testing, just like you would have a local database running. Fake SQS doesn't 6 | persist anything, not even the queues themselves. You'll have to create the 7 | queues everytime you start it. 8 | 9 | This implementation is **not complete** yet, but should be useful already. 10 | 11 | Done so far are: 12 | 13 | * Creating queues 14 | * Deleting queues 15 | * Listing queues (with prefixes) 16 | * Get queue url via the name 17 | * Send messages (and in batch) 18 | * Receive messages (and in batch) 19 | * Deleting messages (and in batch) 20 | * Changing queue attributes (but not all, and no validation) 21 | * Setting visibility timeouts for messages 22 | * Purge Queue 23 | 24 | Certain bits are left off on purpose, to make it easier to work with, such as: 25 | 26 | * No checking on access keys or signatures 27 | * No 60 second delay between deleting a queue and recreating it. 28 | 29 | Other parts are just not done yet: 30 | 31 | * Permissions 32 | * Error handling 33 | 34 | So, actually, just the basics are implemented at this point. 35 | 36 | PS. There is also [Fake SNS] [fake_sns]. 37 | 38 | ## Usage 39 | 40 | To install: 41 | 42 | ``` 43 | $ gem install fake_sqs 44 | ``` 45 | 46 | To start: 47 | 48 | ``` 49 | $ fake_sqs 50 | ``` 51 | 52 | To configure, see the options in the help: 53 | 54 | ``` 55 | $ fake_sqs --help 56 | ``` 57 | 58 | By default, FakeSQS uses an in-memory database (just a hash actually). To make 59 | it persistant, run with: 60 | 61 | ``` 62 | $ fake_sqs --database /path/to/database.yml 63 | ``` 64 | 65 | Messages are not persisted, just the queues. 66 | 67 | This is an example of how to configure the official [aws-sdk gem] [aws-sdk], to 68 | let it talk to Fake SQS. 69 | 70 | ``` ruby 71 | AWS.config( 72 | :use_ssl => false, 73 | :sqs_endpoint => "localhost", 74 | :sqs_port => 4568, 75 | :access_key_id => "access key id", 76 | :secret_access_key => "secret access key" 77 | ) 78 | ``` 79 | 80 | ```javascript 81 | var aws = require('aws-sdk'); 82 | var sqs = new aws.SQS({ 83 | endpoint: 'http://localhost:4568', 84 | apiVersion: '2012-11-05', 85 | accessKeyId: 'access key id', 86 | secretAccessKey: 'secret access key', 87 | region: 'region' 88 | }); 89 | ``` 90 | 91 | If you have the configuration options for other libraries, please give them to 92 | me. 93 | 94 | To reset the entire server, during tests for example, send a DELETE request to 95 | the server. For example: 96 | 97 | ``` 98 | $ curl -X DELETE http://localhost:4568/ 99 | ``` 100 | 101 | Within SQS, after receiving, messages will be available again automatically 102 | after a certain time. While this is not implemented (for now at least), you can 103 | trigger this behavior at at will, with a PUT request. 104 | 105 | ``` 106 | $ curl -X PUT http://localhost:4568/ 107 | ``` 108 | 109 | 110 | ### Test Integration 111 | 112 | When making integration tests for your app, you can easily include Fake SQS. 113 | 114 | Here are the methods you need to run FakeSQS programmatically. 115 | 116 | ``` ruby 117 | require "fake_sqs/test_integration" 118 | 119 | # globally, before the test suite starts: 120 | AWS.config( 121 | use_ssl: false, 122 | sqs_endpoint: "localhost", 123 | sqs_port: 4568, 124 | access_key_id: "fake access key", 125 | secret_access_key: "fake secret key", 126 | ) 127 | fake_sqs = FakeSQS::TestIntegration.new 128 | 129 | # before each test that requires SQS: 130 | fake_sqs.start 131 | 132 | # at the end of the suite: 133 | at_exit { 134 | fake_sqs.stop 135 | } 136 | ``` 137 | 138 | By starting it like this it will start when needed, and reset between each test. 139 | 140 | Here's an example for RSpec to put in `spec/spec_helper.rb`: 141 | 142 | ``` ruby 143 | AWS.config( 144 | use_ssl: false, 145 | sqs_endpoint: "localhost", 146 | sqs_port: 4568, 147 | access_key_id: "fake access key", 148 | secret_access_key: "fake secret key", 149 | ) 150 | 151 | RSpec.configure do |config| 152 | config.treat_symbols_as_metadata_keys_with_true_values = true 153 | config.before(:suite) { $fake_sqs = FakeSQS::TestIntegration.new } 154 | config.before(:each, :sqs) { $fake_sqs.start } 155 | config.after(:suite) { $fake_sqs.stop } 156 | end 157 | ``` 158 | 159 | Now you can use the `:sqs metadata to enable SQS integration: 160 | 161 | ``` ruby 162 | describe "something with sqs", :sqs do 163 | it "should work" do 164 | queue = AWS::SQS.new.queues.create("my-queue") 165 | end 166 | end 167 | ``` 168 | 169 | Supports simulation of message failures by calling api_fail: 170 | ``` 171 | fake_sqs.api_fail() 172 | ``` 173 | 174 | where `````` is ```send_message``` or ```receive_message``` (for now) to simulate a failure when sending or receiving an sqs message 175 | 176 | To clear failures, call clear_failure: 177 | ``` 178 | fake_sqs.clear_failure 179 | ``` 180 | 181 | ## Development 182 | 183 | Run all the specs: 184 | 185 | ``` 186 | $ rake 187 | ``` 188 | 189 | This will run the unit tests, then the acceptance tests for both types of 190 | storage (in-memory and on disk). 191 | 192 | When debugging an acceptance test, you can run it like this, which will redirect 193 | output to the console: 194 | 195 | ``` 196 | $ DEBUG=true SQS_DATABASE=tmp/sqs.yml rspec spec/acceptance 197 | ``` 198 | 199 | 200 | [fake_dynamo]: https://github.com/ananthakumaran/fake_dynamo 201 | [aws-sdk]: https://github.com/amazonwebservices/aws-sdk-for-ruby 202 | [fake_sns]: https://github.com/yourkarma/fake_sns 203 | -------------------------------------------------------------------------------- /spec/acceptance/message_actions_spec.rb: -------------------------------------------------------------------------------- 1 | require "integration_spec_helper" 2 | require "securerandom" 3 | 4 | RSpec.describe "Actions for Messages", :sqs do 5 | 6 | QUEUE_NAME = "test" 7 | 8 | before do 9 | sqs.config.endpoint = $fake_sqs.uri 10 | sqs.create_queue(queue_name: QUEUE_NAME) 11 | end 12 | 13 | let(:sqs) { Aws::SQS::Client.new } 14 | 15 | let(:queue_url) { sqs.get_queue_url(queue_name: QUEUE_NAME).queue_url } 16 | 17 | specify "SendMessage" do 18 | msg = "this is my message" 19 | 20 | result = sqs.send_message( 21 | queue_url: queue_url, 22 | message_body: msg, 23 | ) 24 | 25 | expect(result.md5_of_message_body).to eq Digest::MD5.hexdigest(msg) 26 | expect(result.message_id.size).to eq 36 27 | end 28 | 29 | specify "ReceiveMessage" do 30 | body = "test 123" 31 | 32 | sqs.send_message( 33 | queue_url: queue_url, 34 | message_body: body 35 | ) 36 | 37 | response = sqs.receive_message( 38 | queue_url: queue_url, 39 | ) 40 | 41 | expect(response.messages.size).to eq 1 42 | 43 | expect(response.messages.first.body).to eq body 44 | end 45 | 46 | specify "DeleteMessage" do 47 | sqs.send_message( 48 | queue_url: queue_url, 49 | message_body: "test", 50 | ) 51 | 52 | message1 = sqs.receive_message( 53 | queue_url: queue_url, 54 | ).messages.first 55 | 56 | sqs.delete_message( 57 | queue_url: queue_url, 58 | receipt_handle: message1.receipt_handle, 59 | ) 60 | 61 | let_messages_in_flight_expire 62 | 63 | response = sqs.receive_message( 64 | queue_url: queue_url, 65 | ) 66 | expect(response.messages.size).to eq 0 67 | end 68 | 69 | specify "DeleteMessageBatch" do 70 | sqs.send_message( 71 | queue_url: queue_url, 72 | message_body: "test1" 73 | ) 74 | sqs.send_message( 75 | queue_url: queue_url, 76 | message_body: "test2" 77 | ) 78 | 79 | messages_response = sqs.receive_message( 80 | queue_url: queue_url, 81 | max_number_of_messages: 2, 82 | ) 83 | 84 | entries = messages_response.messages.map { |msg| 85 | { 86 | id: SecureRandom.uuid, 87 | receipt_handle: msg.receipt_handle, 88 | } 89 | } 90 | 91 | sqs.delete_message_batch( 92 | queue_url: queue_url, 93 | entries: entries, 94 | ) 95 | 96 | let_messages_in_flight_expire 97 | 98 | response = sqs.receive_message( 99 | queue_url: queue_url, 100 | ) 101 | expect(response.messages.size).to eq 0 102 | end 103 | 104 | specify "PurgeQueue" do 105 | sqs.send_message( 106 | queue_url: queue_url, 107 | message_body: "test1" 108 | ) 109 | sqs.send_message( 110 | queue_url: queue_url, 111 | message_body: "test2" 112 | ) 113 | 114 | sqs.purge_queue( 115 | queue_url: queue_url, 116 | ) 117 | 118 | response = sqs.receive_message( 119 | queue_url: queue_url, 120 | ) 121 | expect(response.messages.size).to eq 0 122 | end 123 | 124 | specify "SendMessageBatch" do 125 | bodies = %w(a b c) 126 | 127 | sqs.send_message_batch( 128 | queue_url: queue_url, 129 | entries: bodies.map { |bd| 130 | { 131 | id: SecureRandom.uuid, 132 | message_body: bd, 133 | } 134 | } 135 | ) 136 | 137 | messages_response = sqs.receive_message( 138 | queue_url: queue_url, 139 | max_number_of_messages: 3, 140 | ) 141 | 142 | expect(messages_response.messages.map(&:body)).to match_array bodies 143 | end 144 | 145 | specify "set message timeout to 0" do 146 | body = 'some-sample-message' 147 | 148 | sqs.send_message( 149 | queue_url: queue_url, 150 | message_body: body, 151 | ) 152 | 153 | message = sqs.receive_message( 154 | queue_url: queue_url, 155 | ).messages.first 156 | 157 | expect(message.body).to eq body 158 | 159 | sqs.change_message_visibility( 160 | queue_url: queue_url, 161 | receipt_handle: message.receipt_handle, 162 | visibility_timeout: 0 163 | ) 164 | 165 | same_message = sqs.receive_message( 166 | queue_url: queue_url, 167 | ).messages.first 168 | expect(same_message.body).to eq body 169 | end 170 | 171 | specify 'set message timeout and wait for message to come' do 172 | 173 | body = 'some-sample-message' 174 | 175 | sqs.send_message( 176 | queue_url: queue_url, 177 | message_body: body, 178 | ) 179 | 180 | message = sqs.receive_message( 181 | queue_url: queue_url, 182 | ).messages.first 183 | expect(message.body).to eq body 184 | 185 | sqs.change_message_visibility( 186 | queue_url: queue_url, 187 | receipt_handle: message.receipt_handle, 188 | visibility_timeout: 2 189 | ) 190 | 191 | nothing = sqs.receive_message( 192 | queue_url: queue_url, 193 | ) 194 | expect(nothing.messages.size).to eq 0 195 | 196 | sleep(5) 197 | 198 | same_message = sqs.receive_message( 199 | queue_url: queue_url, 200 | ).messages.first 201 | expect(same_message.body).to eq body 202 | end 203 | 204 | specify 'should fail if trying to update the visibility_timeout for a message that is not in flight' do 205 | body = 'some-sample-message' 206 | sqs.send_message( 207 | queue_url: queue_url, 208 | message_body: body, 209 | ) 210 | 211 | message = sqs.receive_message( 212 | queue_url: queue_url, 213 | ).messages.first 214 | expect(message.body).to eq body 215 | 216 | sqs.change_message_visibility( 217 | queue_url: queue_url, 218 | receipt_handle: message.receipt_handle, 219 | visibility_timeout: 0 220 | ) 221 | 222 | expect { 223 | sqs.change_message_visibility( 224 | queue_url: queue_url, 225 | receipt_handle: message.receipt_handle, 226 | visibility_timeout: 30 227 | ) 228 | }.to raise_error(Aws::SQS::Errors::MessageNotInflight) 229 | end 230 | 231 | def let_messages_in_flight_expire 232 | $fake_sqs.expire 233 | end 234 | 235 | end 236 | --------------------------------------------------------------------------------