mirror42-freshbooks.rb 3.0.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +15 -0
  2. data/History.txt +8 -0
  3. data/LICENSE +10 -0
  4. data/Manifest.txt +45 -0
  5. data/README +58 -0
  6. data/Rakefile +27 -0
  7. data/lib/freshbooks.rb +95 -0
  8. data/lib/freshbooks/base.rb +176 -0
  9. data/lib/freshbooks/category.rb +11 -0
  10. data/lib/freshbooks/client.rb +23 -0
  11. data/lib/freshbooks/connection.rb +162 -0
  12. data/lib/freshbooks/estimate.rb +15 -0
  13. data/lib/freshbooks/expense.rb +12 -0
  14. data/lib/freshbooks/invoice.rb +22 -0
  15. data/lib/freshbooks/item.rb +11 -0
  16. data/lib/freshbooks/line.rb +11 -0
  17. data/lib/freshbooks/links.rb +7 -0
  18. data/lib/freshbooks/list_proxy.rb +80 -0
  19. data/lib/freshbooks/payment.rb +13 -0
  20. data/lib/freshbooks/project.rb +12 -0
  21. data/lib/freshbooks/recurring.rb +16 -0
  22. data/lib/freshbooks/response.rb +25 -0
  23. data/lib/freshbooks/schema/definition.rb +20 -0
  24. data/lib/freshbooks/schema/mixin.rb +40 -0
  25. data/lib/freshbooks/staff.rb +13 -0
  26. data/lib/freshbooks/task.rb +12 -0
  27. data/lib/freshbooks/time_entry.rb +12 -0
  28. data/lib/freshbooks/xml_serializer.rb +17 -0
  29. data/lib/freshbooks/xml_serializer/serializers.rb +116 -0
  30. data/script/console +10 -0
  31. data/script/destroy +14 -0
  32. data/script/generate +14 -0
  33. data/test/fixtures/freshbooks_credentials.sample.yml +3 -0
  34. data/test/fixtures/invoice_create_response.xml +4 -0
  35. data/test/fixtures/invoice_get_response.xml +54 -0
  36. data/test/fixtures/invoice_list_response.xml +109 -0
  37. data/test/fixtures/success_response.xml +2 -0
  38. data/test/mock_connection.rb +13 -0
  39. data/test/schema/test_definition.rb +36 -0
  40. data/test/schema/test_mixin.rb +39 -0
  41. data/test/test_base.rb +151 -0
  42. data/test/test_connection.rb +145 -0
  43. data/test/test_helper.rb +48 -0
  44. data/test/test_invoice.rb +125 -0
  45. data/test/test_list_proxy.rb +60 -0
  46. data/test/test_page.rb +50 -0
  47. metadata +157 -0
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module Schema
4
+ class TestDefinition < Test::Unit::TestCase
5
+ def setup
6
+ @definition = FreshBooks::Schema::Definition.new
7
+ end
8
+
9
+ def test_method_missing
10
+ # Empty
11
+ assert_equal 0, @definition.members.size
12
+
13
+ # One type
14
+ @definition.string :name
15
+ assert_equal 1, @definition.members.size
16
+ assert_equal({ :type => :string, :read_only => false }, @definition.members["name"])
17
+
18
+ # Multiple attributes
19
+ @definition.fixnum :version, :po_number
20
+ assert_equal 3, @definition.members.size
21
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["version"])
22
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["po_number"])
23
+
24
+ # Multiple times
25
+ @definition.fixnum :lock_number
26
+ assert_equal 4, @definition.members.size
27
+ assert_equal({ :type => :fixnum, :read_only => false }, @definition.members["lock_number"])
28
+ end
29
+
30
+ def test_method_missing_extra_options
31
+ @definition.fixnum :version, :po_number, :read_only => true
32
+ assert_equal({ :type => :fixnum, :read_only => true }, @definition.members["version"])
33
+ assert_equal({ :type => :fixnum, :read_only => true }, @definition.members["po_number"])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module Schema
4
+ class TestMixin < Test::Unit::TestCase
5
+ def test_define_schema__unique_definition_per_class
6
+ assert MyItem.schema_definition.members.include?("name")
7
+ assert !MyItem.schema_definition.members.include?("name2")
8
+
9
+ assert MyItem2.schema_definition.members.include?("name2")
10
+ assert !MyItem2.schema_definition.members.include?("name")
11
+ end
12
+
13
+ def test_define_schema__creates_attr_accessors
14
+ assert MyItem.public_method_defined?("name")
15
+ assert MyItem.public_method_defined?("name=")
16
+
17
+ assert MyItem2.public_method_defined?("name2")
18
+ assert MyItem2.public_method_defined?("name2=")
19
+ end
20
+
21
+ def test_define_schema__creates_read_only_attr_accessors
22
+ assert MyItem.public_method_defined?("read_only_name")
23
+ assert MyItem.protected_method_defined?("read_only_name=")
24
+ end
25
+ end
26
+
27
+ class MyItem < FreshBooks::Base
28
+ define_schema do |s|
29
+ s.string :name
30
+ s.string :read_only_name, :read_only => true
31
+ end
32
+ end
33
+
34
+ class MyItem2 < FreshBooks::Base
35
+ define_schema do |s|
36
+ s.string :name2
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,151 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestBase < Test::Unit::TestCase
4
+ def test_establish_connection
5
+ assert_nil FreshBooks::Base.connection
6
+
7
+ FreshBooks::Base.establish_connection("company.freshbooks.com", "auth_token")
8
+ connection = FreshBooks::Base.connection
9
+ assert_not_nil connection
10
+ assert_equal "company.freshbooks.com", connection.account_url
11
+ assert_equal "auth_token", connection.auth_token
12
+
13
+ FreshBooks::Base.establish_connection("company2.freshbooks.com", "auth_token2")
14
+ connection = FreshBooks::Base.connection
15
+ assert_not_nil connection
16
+ assert_equal "company2.freshbooks.com", connection.account_url
17
+ assert_equal "auth_token2", connection.auth_token
18
+ end
19
+
20
+ def test_new_from_xml_to_xml__round_trip
21
+ xml = <<-EOS
22
+ <my_item>
23
+ <name>name1</name>
24
+ <amount>4.5</amount>
25
+ <read_only_number>5</read_only_number>
26
+ <number>6</number>
27
+ <visible>1</visible>
28
+ <date>2008-02-01</date>
29
+ <created_at>2008-10-22 13:57:00</created_at>
30
+ <my_address>
31
+ <street1>street1</street1>
32
+ </my_address>
33
+ <my_lines>
34
+ <my_line>
35
+ <description>description</description>
36
+ </my_line>
37
+ </my_lines>
38
+ </my_item>
39
+ EOS
40
+ doc = REXML::Document.new(xml)
41
+
42
+ item = FreshBooks::MyItem.new_from_xml(doc.root)
43
+ assert_equal "name1", item.name
44
+ assert_equal 5, item.read_only_number
45
+ assert_equal 6, item.number
46
+ assert_equal 4.5, item.amount
47
+ assert_equal true, item.visible
48
+ assert_equal Date.new(2008, 2, 1), item.date
49
+ assert_equal DateTime.parse("2008-10-22 13:57:00 -04:00"), item.created_at
50
+ assert_equal "street1", item.my_address.street1
51
+ assert_equal 1, item.my_lines.size
52
+ assert_equal "description", item.my_lines.first.description
53
+
54
+ # If someone knows a good way to compare xml docs where ordering doesn't matter
55
+ # let me know. This can be refactored and improved greatly.
56
+ xml_out = item.to_xml.to_s.strip
57
+ assert_equal "<my_item>", xml_out.first(9)
58
+ assert xml_out.include?("<name>name1</name>")
59
+ assert xml_out.include?("<amount>4.5</amount>")
60
+ assert xml_out.include?("<visible>1</visible>")
61
+ assert !xml_out.include?("<read_only_number>5</read_only_number>") # this is read only
62
+ assert xml_out.include?("<number>6</number>")
63
+ assert xml_out.include?("<date>2008-02-01</date>")
64
+ assert xml_out.include?("<created_at>2008-10-22 13:57:00</created_at>")
65
+ assert xml_out.include?("<my_address><street1>street1</street1></my_address>")
66
+ assert xml_out.include?("<my_lines><my_line><description>description</description></my_line></my_lines>")
67
+ end
68
+
69
+ def test_create_objects_with_initializer_arguments
70
+ invoice_1 = FreshBooks::Invoice.new
71
+ invoice_1.client_id = 1
72
+ invoice_1.lines = [FreshBooks::Line.new, FreshBooks::Line.new]
73
+ invoice_1.lines[0].name = "Awesomeness"
74
+ invoice_1.lines[0].unit_cost = 9999
75
+ invoice_1.lines[0].quantity = 42
76
+ invoice_1.lines[1].name = "Ninja skills"
77
+ invoice_1.lines[1].unit_cost = 349
78
+ invoice_1.lines[1].quantity = 100
79
+
80
+ invoice_2 = FreshBooks::Invoice.new(
81
+ :client_id => 1,
82
+ :lines => [
83
+ FreshBooks::Line.new(
84
+ :name => "Awesomeness",
85
+ :unit_cost => 9999,
86
+ :quantity => 42
87
+ ),
88
+ FreshBooks::Line.new(
89
+ :name => "Ninja skills",
90
+ :unit_cost => 349,
91
+ :quantity => 100
92
+ )
93
+ ]
94
+ )
95
+
96
+ assert_equal invoice_1.to_xml, invoice_2.to_xml
97
+ end
98
+
99
+ def test_can_handle_all_zero_updated_at
100
+ xml = <<-END_XML
101
+ <my_client>
102
+ <client_id>3</client_id>
103
+ <first_name>Test</first_name>
104
+ <last_name>User</last_name>
105
+ <organization>User Testing</organization>
106
+ <updated>0000-00-00 00:00:00</updated>
107
+ </my_client>
108
+ END_XML
109
+ doc = REXML::Document.new(xml)
110
+
111
+ item = FreshBooks::MyClient.new_from_xml(doc.root)
112
+ assert_equal nil, item.updated
113
+ end
114
+
115
+ end
116
+
117
+ module FreshBooks
118
+ class MyItem < FreshBooks::Base
119
+ define_schema do |s|
120
+ s.string :name
121
+ s.fixnum :read_only_number, :read_only => true
122
+ s.fixnum :number
123
+ s.float :amount
124
+ s.boolean :visible
125
+ s.date :date
126
+ s.date_time :created_at
127
+ s.object :my_address
128
+ s.array :my_lines
129
+ end
130
+ end
131
+
132
+ class MyClient < FreshBooks::Base
133
+ define_schema do |s|
134
+ s.string :first_name, :last_name, :organization
135
+ s.fixnum :client_id
136
+ s.date_time :updated
137
+ end
138
+ end
139
+
140
+ class MyAddress < FreshBooks::Base
141
+ define_schema do |s|
142
+ s.string :street1
143
+ end
144
+ end
145
+
146
+ class MyLine < FreshBooks::Base
147
+ define_schema do |s|
148
+ s.string :description
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,145 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestConnection < Test::Unit::TestCase
4
+ def setup
5
+ @connection = FreshBooks::Connection.new("company.freshbooks.com", "auth_token")
6
+ end
7
+
8
+ def test_connection_accessors
9
+ assert_equal "company.freshbooks.com", @connection.account_url
10
+ assert_equal "auth_token", @connection.auth_token
11
+ end
12
+
13
+ def test_connection_request_headers
14
+ request_headers = { "key" => "value" }
15
+ connection = FreshBooks::Connection.new("company.freshbooks.com", "auth_token", request_headers)
16
+ assert_equal request_headers, connection.request_headers
17
+ end
18
+
19
+
20
+ def test_create_request__array_of_elements
21
+ request = @connection.send(:create_request, 'mymethod', [['element1', 'value1'], [:element2, :value2]])
22
+ assert_equal "<?xml version='1.0' encoding='UTF-8'?><request method='mymethod'><element1>value1</element1><element2>value2</element2></request>", request
23
+ end
24
+
25
+ def test_create_request__base_object_element
26
+ invoice = FreshBooks::Invoice.new
27
+ invoice.expects(:to_xml).with().returns("<invoice><number>23</number></invoice>")
28
+
29
+ request = @connection.send(:create_request, 'mymethod', 'invoice' => invoice)
30
+ assert_equal "<?xml version='1.0' encoding='UTF-8'?><request method='mymethod'><invoice><number>23</number></invoice></request>", request
31
+ end
32
+
33
+ def test_check_for_api_error__success
34
+ body = "body xml"
35
+ response = Net::HTTPSuccess.new("1.1", "200", "message")
36
+ response.expects(:body).with().returns(body)
37
+ assert_equal body, @connection.send(:check_for_api_error, response)
38
+ end
39
+
40
+ def test_check_for_api_error__unknown_system
41
+ response = Net::HTTPMovedPermanently.new("1.1", "301", "message")
42
+ response.stubs(:[]).with("location").returns("loginSearch")
43
+ assert_raise(FreshBooks::UnknownSystemError) do
44
+ @connection.send(:check_for_api_error, response)
45
+ end
46
+ end
47
+
48
+ def test_check_for_api_error__deactivated
49
+ response = Net::HTTPMovedPermanently.new("1.1", "301", "message")
50
+ response.stubs(:[]).with("location").returns("deactivated")
51
+ assert_raise(FreshBooks::AccountDeactivatedError) do
52
+ @connection.send(:check_for_api_error, response)
53
+ end
54
+ end
55
+
56
+ def test_check_for_api_error__unauthorized
57
+ response = Net::HTTPUnauthorized.new("1.1", "401", "message")
58
+ assert_raise(FreshBooks::AuthenticationError) do
59
+ @connection.send(:check_for_api_error, response)
60
+ end
61
+ end
62
+
63
+ def test_check_for_api_error__bad_request
64
+ response = Net::HTTPBadRequest.new("1.1", "401", "message")
65
+ assert_raise(FreshBooks::ApiAccessNotEnabledError) do
66
+ @connection.send(:check_for_api_error, response)
67
+ end
68
+ end
69
+
70
+ def test_check_for_api_error__internal_error
71
+ response = Net::HTTPBadGateway.new("1.1", "502", "message")
72
+ assert_raise(FreshBooks::InternalError) do
73
+ @connection.send(:check_for_api_error, response)
74
+ end
75
+
76
+ response = Net::HTTPMovedPermanently.new("1.1", "301", "message")
77
+ response.stubs(:[]).with("location").returns("somePage")
78
+ assert_raise(FreshBooks::InternalError) do
79
+ @connection.send(:check_for_api_error, response)
80
+ end
81
+ end
82
+
83
+ def test_close_is_only_called_once_in_ntexted_start_sessions
84
+ @connection.expects(:obtain_connection)
85
+ @connection.expects(:close)
86
+
87
+ @connection.start_session { @connection.start_session { } }
88
+ end
89
+
90
+ def test_reconnect
91
+ connection = stub()
92
+
93
+ @connection.expects(:close).with()
94
+ @connection.expects(:obtain_connection).with(true).returns(connection)
95
+
96
+ assert_equal connection, @connection.send(:reconnect)
97
+ end
98
+
99
+ def test_post_request_successfull_request
100
+ request = "<request></request>"
101
+ response = "<response></response>"
102
+
103
+ http_connection = stub()
104
+ http_connection.expects(:request).with(request).returns(response)
105
+ @connection.expects(:start_session).with().yields(http_connection)
106
+
107
+ assert_equal response, @connection.send(:post_request, request)
108
+ end
109
+
110
+ def test_post_request_eof_error_retry
111
+ request = "<request></request>"
112
+ response = "<response></response>"
113
+ eof_error = EOFError.new("End of file error")
114
+
115
+ bad_http_connection = stub()
116
+ bad_http_connection.expects(:request).with(request).raises(eof_error)
117
+
118
+ new_http_connection = stub()
119
+ new_http_connection.expects(:request).with(request).returns(response)
120
+
121
+ @connection.expects(:start_session).with().yields(bad_http_connection)
122
+ @connection.expects(:reconnect).with().returns(new_http_connection)
123
+
124
+ assert_equal response, @connection.send(:post_request, request)
125
+ end
126
+
127
+ def test_post_request_eof_error_retry_only_retry_once
128
+ request = "<request></request>"
129
+ response = "<response></response>"
130
+ eof_error = EOFError.new("End of file error")
131
+
132
+ bad_http_connection = stub()
133
+ bad_http_connection.expects(:request).with(request).raises(eof_error)
134
+
135
+ new_http_connection = stub()
136
+ new_http_connection.expects(:request).with(request).raises(eof_error)
137
+
138
+ @connection.expects(:start_session).with().yields(bad_http_connection)
139
+ @connection.expects(:reconnect).with().returns(new_http_connection)
140
+
141
+ assert_raises(EOFError, eof_error.message) do
142
+ @connection.send(:post_request, request)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,48 @@
1
+ require File.dirname(__FILE__) + '/../lib/freshbooks'
2
+
3
+ require 'stringio'
4
+ require 'test/unit'
5
+ require File.dirname(__FILE__) + '/mock_connection'
6
+
7
+ begin
8
+ require 'mocha'
9
+ rescue LoadError
10
+ require 'rubygems'
11
+ gem 'mocha'
12
+ require 'mocha'
13
+ end
14
+
15
+ class Test::Unit::TestCase
16
+
17
+ @@fixtures = {}
18
+ def self.fixtures list
19
+ [list].flatten.each do |fixture|
20
+ self.class_eval do
21
+ # add a method name for this fixture type
22
+ define_method(fixture) do |item|
23
+ # load and cache the YAML
24
+ @@fixtures[fixture] ||= YAML::load_file(self.fixture_dir + "/#{fixture.to_s}.yml")
25
+ @@fixtures[fixture][item.to_s]
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+ def mock_connection(file_name)
33
+ mock_connection = MockConnection.new(fixture_xml_content(file_name))
34
+ FreshBooks::Base.stubs(:connection).with().returns(mock_connection)
35
+ mock_connection
36
+ end
37
+
38
+ def fixture_xml_content(file_name)
39
+ # Quick way to remove white space and newlines from xml. Makes it easier to compare in tests
40
+ open(File.join(fixture_dir, "#{file_name}.xml"), "r").readlines.inject("") do |contents, line|
41
+ contents + line.strip
42
+ end
43
+ end
44
+
45
+ def fixture_dir
46
+ File.join(File.dirname(__FILE__), "fixtures")
47
+ end
48
+ end
@@ -0,0 +1,125 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestInvoice < Test::Unit::TestCase
4
+ def test_list
5
+ mock_call_api("invoice.list", { "page" => 1 }, "invoice_list_response")
6
+
7
+ invoices = FreshBooks::Invoice.list
8
+ assert_equal 3, invoices.size
9
+ assert_invoice invoices[0], 0
10
+ assert_invoice invoices[1], 1
11
+ end
12
+
13
+ def test_get
14
+ invoice_id = 2
15
+ mock_call_api("invoice.get", { "invoice_id" => invoice_id }, "invoice_get_response")
16
+
17
+ invoice = FreshBooks::Invoice.get(invoice_id)
18
+ assert_invoice invoice, 0, true
19
+ end
20
+
21
+ def test_create
22
+ invoice = FreshBooks::Invoice.new
23
+ assert_nil invoice.invoice_id
24
+
25
+ mock_call_api("invoice.create", { "invoice" => invoice }, "invoice_create_response")
26
+ assert invoice.create
27
+ assert_equal 1, invoice.invoice_id
28
+ end
29
+
30
+ def test_update
31
+ invoice = FreshBooks::Invoice.new
32
+ invoice.invoice_id = 1
33
+
34
+ mock_call_api("invoice.update", { "invoice" => invoice }, "success_response")
35
+ assert invoice.update
36
+ end
37
+
38
+ def test_delete
39
+ invoice = FreshBooks::Invoice.new
40
+ invoice.invoice_id = 2
41
+ mock_call_api("invoice.delete", { "invoice_id" => invoice.invoice_id }, "success_response")
42
+
43
+ assert invoice.delete
44
+ end
45
+
46
+ def test_send_by_email
47
+ invoice = FreshBooks::Invoice.new
48
+ invoice.invoice_id = 2
49
+ mock_call_api("invoice.sendByEmail", { "invoice_id" => invoice.invoice_id }, "success_response")
50
+
51
+ assert invoice.send_by_email
52
+ end
53
+
54
+ def test_send_by_snail_mail
55
+ invoice = FreshBooks::Invoice.new
56
+ invoice.invoice_id = 2
57
+ mock_call_api("invoice.sendBySnailMail", { "invoice_id" => invoice.invoice_id }, "success_response")
58
+
59
+ assert invoice.send_by_snail_mail
60
+ end
61
+
62
+ private
63
+
64
+ def mock_call_api(method, options, response_fixture)
65
+ FreshBooks::Base.connection.
66
+ expects(:call_api).
67
+ with(method, options).
68
+ returns(FreshBooks::Response.new(fixture_xml_content(response_fixture)))
69
+ end
70
+
71
+ def assert_invoice(invoice, number, expanded_form = false)
72
+ number = number + 1
73
+
74
+ assert_equal number, invoice.invoice_id
75
+ assert_equal "number#{number}", invoice.number
76
+ assert_equal number, invoice.client_id
77
+ assert_equal number, invoice.recurring_id
78
+ assert_equal "draft", invoice.status
79
+ assert_equal (number * 100), invoice.amount
80
+ assert_equal (number * 100) / 2, invoice.amount_outstanding
81
+ assert_equal Date.parse("2009-02-0#{number}"), invoice.date
82
+
83
+ assert_equal number, invoice.po_number
84
+ assert_equal number.to_f, invoice.discount
85
+ assert_equal "notes#{number}", invoice.notes
86
+ assert_equal "terms#{number}", invoice.terms
87
+ assert_equal "first_name#{number}", invoice.first_name
88
+ assert_equal "last_name#{number}", invoice.last_name
89
+ assert_equal "p_street1#{number}", invoice.p_street1
90
+ assert_equal "p_street2#{number}", invoice.p_street2
91
+ assert_equal "p_city#{number}", invoice.p_city
92
+ assert_equal "p_state#{number}", invoice.p_state
93
+ assert_equal "p_country#{number}", invoice.p_country
94
+ assert_equal "p_code#{number}", invoice.p_code
95
+
96
+ assert_equal "return_uri#{number}", invoice.return_uri
97
+ assert_equal DateTime.parse("2009-08-#{number} 0#{number}:00:00 -04:00"), invoice.updated
98
+
99
+
100
+
101
+ assert_equal "client_view#{number}", invoice.links.client_view
102
+ assert_equal "view#{number}", invoice.links.view
103
+ assert_equal "edit#{number}", invoice.links.edit
104
+
105
+ lines = invoice.lines
106
+ assert_equal 1, lines.size
107
+ lines.each_with_index do |line, index|
108
+ assert_line(line, index)
109
+ end
110
+ end
111
+
112
+ def assert_line(line, number)
113
+ number = number + 1
114
+
115
+ assert_equal number.to_f, line.amount
116
+ assert_equal "name#{number}", line.name
117
+ assert_equal "description#{number}", line.description
118
+ assert_equal number.to_f, line.unit_cost
119
+ assert_equal number, line.quantity
120
+ assert_equal "tax1_name#{number}", line.tax1_name
121
+ assert_equal "tax2_name#{number}", line.tax2_name
122
+ assert_equal number.to_f, line.tax1_percent
123
+ assert_equal number.to_f, line.tax2_percent
124
+ end
125
+ end