mirror42-freshbooks.rb 3.0.25

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.
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