hushed 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ module Hushed
2
+ class Message
3
+
4
+ NAMESPACE = "http://schemas.quietlogistics.com/V2/EventMessage.xsd"
5
+
6
+ class MissingDocumentError < StandardError; end
7
+ class MissingClientError < StandardError; end
8
+
9
+ attr_reader :client, :document, :xml
10
+
11
+ def initialize(options = {})
12
+ @xml = Nokogiri::XML::Document.parse(options[:xml]) if options[:xml]
13
+ @client = options[:client]
14
+ @document = options[:document]
15
+ end
16
+
17
+ def to_xml
18
+ builder = Nokogiri::XML::Builder.new do |xml|
19
+ xml.EventMessage(attributes)
20
+ end
21
+ builder.to_xml
22
+ end
23
+
24
+ def document_type
25
+ @document ? @document.type : @xml.css('EventMessage').first['DocumentType']
26
+ end
27
+
28
+ def document_name
29
+ @document ? @document.filename : @xml.css('EventMessage').first['DocumentName']
30
+ end
31
+
32
+ def attributes
33
+ raise(MissingClientError.new("client cannot be missing")) unless @client
34
+ raise(MissingDocumentError.new("document cannot be missing")) unless @document
35
+ {
36
+ ClientId: @client.client_id, BusinessUnit: @client.business_unit,
37
+ DocumentName: @document.filename, DocumentType: @document.type,
38
+ Warehouse: @document.warehouse, MessageDate: @document.date.utc,
39
+ MessageId: @document.message_id, xmlns: NAMESPACE
40
+ }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ module Hushed
2
+ class Queue
3
+ attr_reader :client
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def send(message)
9
+ queue = client.to_quiet_queue
10
+ queue.send_message(message.to_xml)
11
+ end
12
+
13
+ def receive
14
+ queue = client.from_quiet_queue
15
+ message = nil
16
+ queue.receive_message do |msg|
17
+ message = Message.new(xml: msg.body)
18
+ end
19
+ message || Message.new
20
+ end
21
+
22
+ def approximate_pending_messages
23
+ client.from_quiet_queue.approximate_number_of_messages
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ require 'hushed/documents/request/shipment_order'
2
+
3
+ module Hushed
4
+ module Request
5
+ VALID_REQUEST_TYPES = %w(ShipmentOrder)
6
+
7
+ def self.valid_type?(type)
8
+ VALID_REQUEST_TYPES.include?(type)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'hushed/documents/response/shipment_order_result'
2
+
3
+ module Hushed
4
+ module Response
5
+ VALID_RESPONSE_TYPES = %w(ShipmentOrderResult)
6
+
7
+ def self.valid_type?(type)
8
+ VALID_RESPONSE_TYPES.include?(type)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Hushed
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,12 @@
1
+ access_key_id: abracadabra
2
+ secret_access_key: alakazam
3
+ client_id: HUSHED
4
+ business_unit: HUSHED
5
+ warehouse: SPACE
6
+ buckets:
7
+ to: hushed-to-quiet
8
+ from: hushed-from-quiet
9
+ queues:
10
+ to: http://queue.amazonaws/1234567890/hushed_to_quiet
11
+ from: http://queue.amazonaws/1234567890/hushed_from_quiet
12
+ inventory: http://queue.amazonaws/1234567890/hushed_inventory
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <SOResult
3
+ xmlns="http://schemas.quiettechnology.com/V2/SOResultDocument.xsd"
4
+ ClientID="HUSHED"
5
+ BusinessUnit="HUSHED"
6
+ CartonCount="1"
7
+ DateShipped="2009-09-01T00:00:00Z"
8
+ FreightCost="10.00"
9
+ OrderNumber="1234567890">
10
+ <Line Line="1" Quantity="1" />
11
+ <Line Line="2" Quantity="1" />
12
+ <Carton
13
+ Carrier="USPS"
14
+ CartonId="S12345678901"
15
+ CartonNumber="1"
16
+ FreightCost="10.00"
17
+ ServiceLevel="FIRST"
18
+ TrackingId="40000000000"
19
+ Weight="0.66">
20
+ <Content Line="1"Quantity="1" />
21
+ <Content Line="2"Quantity="1" />
22
+ </Carton>
23
+ </SOResult>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <EventMessage
3
+ xmlns="http://schemas.quietlogistics.com/V2/EventMessage.xsd"
4
+ ClientId="HUSHED"
5
+ BusinessUnit="HUSHED"
6
+ DocumentName="HUSHED_PurchaseOrder_1234_20100927_132505124.xml"
7
+ DocumentType="PurchaseOrder"
8
+ MessageId="EF1CE966-38A2-428b-BA67-EFF23AF22F57"
9
+ Warehouse="CORP1"
10
+ MessageDate="2009-09-01T12:00:00Z">
11
+ </EventMessage>
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'hushed'
3
+ require 'hushed/blackboard'
4
+
5
+ module Hushed
6
+ describe "BlackboardRemote" do
7
+ include Configuration
8
+ include Fixtures
9
+
10
+ before do
11
+ AWS.config(stub_requests: false)
12
+ @client = Client.new(load_configuration)
13
+ @blackboard = Blackboard.new(@client)
14
+ @document = DocumentDouble.new(
15
+ message_id: '1234567',
16
+ date: Time.new(2013, 04, 05, 12, 30, 15).utc,
17
+ filename: 'neat_beans.xml',
18
+ client: @client,
19
+ type: 'ShipmentOrderResult'
20
+ )
21
+ end
22
+
23
+ after do
24
+ buckets = [@client.to_quiet_bucket, @client.from_quiet_bucket]
25
+ buckets.each { |bucket| bucket.objects.delete_all }
26
+ end
27
+
28
+ it "should be able to write a document to an S3 bucket" do
29
+ message = @blackboard.post(@document)
30
+ file = message.document_name
31
+ bucket = @client.to_quiet_bucket
32
+ assert bucket.objects[file].exists?, "It appears that #{file} was not written to S3"
33
+ end
34
+
35
+ it "should be able to fetch a document from an S3 bucket when given a message" do
36
+ expected_contents = load_response('shipment_order_result').read()
37
+ @client.from_quiet_bucket.objects[@document.filename].write(expected_contents)
38
+ message = MessageDouble.new(document_name: @document.filename, document_type: @document.type)
39
+ document = @blackboard.fetch(message)
40
+ assert_equal expected_contents, document.io
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'openssl'
3
+ require 'hushed'
4
+ require 'hushed/message'
5
+
6
+ module Hushed
7
+ describe "QueueRemote" do
8
+ include Configuration
9
+
10
+ before do
11
+ AWS.config(:stub_requests => false)
12
+ @client = Client.new(load_configuration)
13
+ @sqs_queues = [@client.to_quiet_queue, @client.from_quiet_queue]
14
+ @default_wait_time = @sqs_queues.map(&:wait_time_seconds).max
15
+ @sqs_queues.each { |queue| queue.wait_time_seconds = 1 }
16
+
17
+ @document = DocumentDouble.new(
18
+ :message_id => '1234567',
19
+ :date => Time.new(2013, 04, 05, 12, 30, 15).utc,
20
+ :filename => 'neat_beans.xml',
21
+ :client => @client,
22
+ :type => 'Thinger'
23
+ )
24
+
25
+ @message = Message.new(:client => @client, :document => @document)
26
+ @queue = Queue.new(@client)
27
+ end
28
+
29
+ after do
30
+ @sqs_queues.each do |queue|
31
+ flush(queue)
32
+ end
33
+ @sqs_queues.each { |queue| queue.wait_time_seconds = @default_wait_time }
34
+ end
35
+
36
+ it "should be able to push a message onto the queue" do
37
+ expected_md5 = OpenSSL::Digest::MD5.new.hexdigest(@message.to_xml)
38
+ sent_message = @queue.send(@message)
39
+ assert_equal 1, @client.to_quiet_queue.approximate_number_of_messages
40
+ assert_equal expected_md5, sent_message.md5
41
+ end
42
+
43
+ it "should be able to fetch a message from the queue" do
44
+ @client.from_quiet_queue.send_message(@message.to_xml)
45
+ message = @queue.receive
46
+ assert_equal @message.to_xml, message.xml.to_xml
47
+ assert_equal @message.document_type, message.document_type
48
+ assert_equal @message.document_name, message.document_name
49
+ end
50
+
51
+ private
52
+ def flush(queue)
53
+ pending_messages = queue.approximate_number_of_messages
54
+ while pending_messages > 0
55
+ queue.receive_message do |message|
56
+ message.delete
57
+ pending_messages -= 1
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,169 @@
1
+ require 'minitest/autorun'
2
+ require 'nokogiri'
3
+ require 'yaml'
4
+ require 'mocha/setup'
5
+ require 'hushed'
6
+
7
+ module Fixtures
8
+ def load_fixture(path)
9
+ File.open(path, 'rb')
10
+ end
11
+
12
+ def load_response(response_name)
13
+ load_fixture("spec/fixtures/documents/responses/#{response_name}.xml")
14
+ end
15
+
16
+ def load_message(message_name)
17
+ load_fixture("spec/fixtures/messages/#{message_name}.xml")
18
+ end
19
+ end
20
+
21
+ module Configuration
22
+ def load_configuration
23
+ test_credentials_file = ENV['HOME'] + '/.hushed/credentials.yml'
24
+ test_credentials_file = "spec/fixtures/credentials.yml" unless File.exists?(test_credentials_file)
25
+ YAML.load(File.open(test_credentials_file, 'rb'))
26
+ end
27
+ end
28
+
29
+ class LineItemDouble
30
+ DEFAULT_OPTIONS = {
31
+ :id => 123456,
32
+ :quantity => 1,
33
+ :unit_of_measure => 'EA',
34
+ :price => '12.95'
35
+ }
36
+
37
+ attr_reader :id, :quantity, :unit_of_measure, :price
38
+ def initialize(options = {})
39
+ @id = options[:id]
40
+ @quantity = options[:quantity]
41
+ @unit_of_measure = options[:unit_of_measure]
42
+ @price = options[:price]
43
+ end
44
+
45
+ def self.example(options = {})
46
+ self.new(DEFAULT_OPTIONS.merge(options))
47
+ end
48
+ end
49
+
50
+ class AddressDouble
51
+ DEFAULT_OPTIONS = {
52
+ :company => 'Shopify',
53
+ :name => 'John Smith',
54
+ :address1 => '123 Fake St',
55
+ :address2 => 'Unit 128',
56
+ :city => 'Ottawa',
57
+ :province_code => 'ON',
58
+ :country_code => 'CA',
59
+ :zip => 'K1N 5T5'
60
+ }
61
+
62
+ attr_reader :company, :name, :address1, :address2, :city, :province_code
63
+ attr_reader :country_code, :zip
64
+ def initialize(options = {})
65
+ @company = options[:company]
66
+ @name = options[:name]
67
+ @address1 = options[:address1]
68
+ @address2 = options[:address2]
69
+ @city = options[:city]
70
+ @province_code = options[:province_code]
71
+ @country_code = options[:country_code]
72
+ @zip = options[:zip]
73
+ end
74
+
75
+ def self.example(options = {})
76
+ self.new(DEFAULT_OPTIONS.merge(options))
77
+ end
78
+ end
79
+
80
+ class ShippingLineDouble
81
+ def initialize(options = {})
82
+ @options = options
83
+ end
84
+
85
+ def carrier
86
+ @options[:code].split('_').first
87
+ end
88
+
89
+ def service_level
90
+ @options[:code].split('_').last
91
+ end
92
+ end
93
+
94
+ class OrderDouble
95
+ DEFAULT_OPTIONS = {
96
+ :line_items => [LineItemDouble.example],
97
+ :shipping_address => AddressDouble.example,
98
+ :billing_address => AddressDouble.example,
99
+ :note => 'Happy Birthday',
100
+ :created_at => Time.new(2013, 04, 05, 12, 30, 00),
101
+ :id => 123456,
102
+ :shipping_lines => [ShippingLineDouble.new(code: "FEDEX_GROUND", price: "34.40", source: "fedex", title: "FedEx Ground")],
103
+ :email => 'john@smith.com',
104
+ :total_price => '123.45'
105
+ }
106
+
107
+ attr_reader :line_items, :shipping_address, :billing_address, :note, :email
108
+ attr_reader :total_price, :email, :id, :type, :created_at, :shipping_lines
109
+ def initialize(options = {})
110
+ @line_items = options[:line_items]
111
+ @shipping_address = options[:shipping_address]
112
+ @billing_address = options[:billing_address]
113
+ @note = options[:note]
114
+ @created_at = options[:created_at]
115
+ @id = options[:id]
116
+ @shipping_lines = options[:shipping_lines]
117
+ @email = options[:email]
118
+ @total_price = options[:total_price]
119
+ end
120
+
121
+ def self.example(options = {})
122
+ self.new(DEFAULT_OPTIONS.merge(options))
123
+ end
124
+
125
+ def type
126
+ @type || "SO"
127
+ end
128
+ end
129
+
130
+ class DocumentDouble
131
+ include Hushed::Documents::Document
132
+ attr_accessor :type, :message_id, :warehouse, :date, :client
133
+ attr_accessor :business_unit, :document_number, :io
134
+
135
+ def initialize(options = {})
136
+ options.each do |key, value|
137
+ self.public_send("#{key}=".to_sym, value)
138
+ end
139
+ end
140
+
141
+ def to_xml
142
+ builder = Nokogiri::XML::Builder.new do |xml|
143
+ xml.DocumentDouble 'Hello World'
144
+ end
145
+ builder.to_xml
146
+ end
147
+
148
+ def filename=(filename)
149
+ @filename = filename
150
+ end
151
+ end
152
+
153
+ class MessageDouble
154
+ attr_accessor :document_name, :document_type
155
+
156
+ def initialize(options = {})
157
+ @document_name = options[:document_name]
158
+ @document_type = options[:document_type]
159
+ end
160
+ end
161
+
162
+ class ClientDouble
163
+ attr_accessor :client_id, :business_unit
164
+
165
+ def initialize(options = {})
166
+ @client_id = options[:client_id]
167
+ @business_unit = options[:business_unit]
168
+ end
169
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+ require 'hushed/blackboard'
3
+
4
+ module Hushed
5
+ module Documents
6
+ module Response
7
+ class ThingerResponse
8
+ attr_reader :contents
9
+ def initialize(contents)
10
+ @contents = contents
11
+ end
12
+ end
13
+ end
14
+
15
+ module Request
16
+ class ThingerRequest
17
+ attr_reader :contents
18
+ def initialize(contents)
19
+ @contents = contents
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module Hushed
27
+ describe "Blackboard" do
28
+ before do
29
+ @bucket = mock()
30
+
31
+ @client = mock()
32
+ @client.stubs(:to_quiet_bucket).returns(@bucket)
33
+ @client.stubs(:from_quiet_bucket).returns(@bucket)
34
+
35
+ @io = StringIO.new
36
+
37
+ @document = mock()
38
+ @document.stubs(:to_xml).returns("actually doesn't matter")
39
+ @document.stubs(:filename).returns("abracadabra_1234_xyz.xml")
40
+
41
+ @message = mock()
42
+ @message.stubs(:document_type).returns('ShipmentOrderResult')
43
+ @message.stubs(:document_name).returns('abracadabra_4321_zyx.xml')
44
+
45
+ @blackboard = Blackboard.new(@client)
46
+ end
47
+
48
+ it "should be possible to post a document to the blackboard" do
49
+ @bucket.expects(:objects).returns({@document.filename => @io})
50
+ @blackboard.post(@document)
51
+ assert_io(@document.to_xml, @io)
52
+ end
53
+
54
+ it "should be possible to build a document for a response type" do
55
+ Response.expects(:valid_type?).returns(true)
56
+ response = @blackboard.build_document('ThingerResponse', 'thinger')
57
+ assert_equal 'thinger', response.contents[:io]
58
+ end
59
+
60
+ it "should be possible to build a document for a request type" do
61
+ Request.expects(:valid_type?).returns(true)
62
+ response = @blackboard.build_document('ThingerRequest', 'thinger')
63
+ assert_equal 'thinger', response.contents[:io]
64
+ end
65
+
66
+ it "should return nil if the type was not valid" do
67
+ assert_equal nil, @blackboard.build_document('ThingerRequest', 'thinger')
68
+ end
69
+
70
+ it "should be possible to fetch a document from the blackboard" do
71
+ @io.write('fancy noodles')
72
+ @io.rewind
73
+ @bucket.expects(:objects).returns({@message.document_name => @io})
74
+ Response.expects(:valid_type?).returns(true)
75
+ @message.stubs(:document_type).returns('ThingerResponse')
76
+ assert_equal 'fancy noodles', @blackboard.fetch(@message).contents[:io]
77
+ end
78
+
79
+ it "should be possible to remove a document from the blackboard" do
80
+ s3object = mock()
81
+ s3object.expects(:delete)
82
+ s3object.expects(:exists?).returns(true)
83
+
84
+ @bucket.expects(:objects).returns({@message.document_name => s3object})
85
+ assert_equal true, @blackboard.remove(@message)
86
+ end
87
+
88
+ it "should not raise an exception if the document for removal couldn't be found" do
89
+ s3object = mock()
90
+ s3object.expects(:delete).never
91
+ s3object.expects(:exists?).returns(false)
92
+
93
+ @bucket.expects(:objects).returns({@message.document_name => s3object})
94
+ assert_equal false, @blackboard.remove(@message)
95
+ end
96
+
97
+ def assert_io(expectation, io)
98
+ io.rewind
99
+ assert_equal expectation, io.read
100
+ end
101
+ end
102
+ end