hushed 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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