harvest 0.8.2

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 (38) hide show
  1. data/HISTORY +3 -0
  2. data/LICENSE +23 -0
  3. data/README.rdoc +173 -0
  4. data/Rakefile +44 -0
  5. data/lib/harvest.rb +35 -0
  6. data/lib/harvest/base.rb +77 -0
  7. data/lib/harvest/harvest_resource.rb +15 -0
  8. data/lib/harvest/plugins/active_resource_inheritable_headers.rb +36 -0
  9. data/lib/harvest/plugins/toggleable.rb +12 -0
  10. data/lib/harvest/resources/client.rb +8 -0
  11. data/lib/harvest/resources/entry.rb +34 -0
  12. data/lib/harvest/resources/expense.rb +22 -0
  13. data/lib/harvest/resources/expense_category.rb +7 -0
  14. data/lib/harvest/resources/person.rb +44 -0
  15. data/lib/harvest/resources/project.rb +51 -0
  16. data/lib/harvest/resources/task.rb +8 -0
  17. data/lib/harvest/resources/task_assignment.rb +27 -0
  18. data/lib/harvest/resources/user_assignment.rb +27 -0
  19. data/test/integration/client_integration.rb +17 -0
  20. data/test/integration/client_teardown.rb +11 -0
  21. data/test/integration/expense_category_integration.rb +16 -0
  22. data/test/integration/expense_category_teardown.rb +12 -0
  23. data/test/integration/harvest_integration_test.rb +47 -0
  24. data/test/integration/project_integration.rb +19 -0
  25. data/test/integration/project_teardown.rb +12 -0
  26. data/test/integration/task_integration.rb +19 -0
  27. data/test/integration/task_teardown.rb +12 -0
  28. data/test/test_helper.rb +51 -0
  29. data/test/unit/base_test.rb +88 -0
  30. data/test/unit/resources/client_test.rb +82 -0
  31. data/test/unit/resources/expense_category_test.rb +49 -0
  32. data/test/unit/resources/expense_test.rb +14 -0
  33. data/test/unit/resources/person_test.rb +150 -0
  34. data/test/unit/resources/project_test.rb +154 -0
  35. data/test/unit/resources/task_assignment_test.rb +72 -0
  36. data/test/unit/resources/task_test.rb +82 -0
  37. data/test/unit/resources/user_assignment_test.rb +71 -0
  38. metadata +111 -0
@@ -0,0 +1,22 @@
1
+ module Harvest
2
+ module Resources
3
+ class Expense < Harvest::HarvestResource
4
+
5
+ self.element_name = "expense"
6
+
7
+ class << self
8
+
9
+ def person_id=(id)
10
+ @person_id = id
11
+ self.site = self.site + "/people/#{@person_id}"
12
+ end
13
+
14
+ def person_id
15
+ @person_id
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ module Harvest
2
+ module Resources
3
+ class ExpenseCategory < Harvest::HarvestResource
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,44 @@
1
+ module Harvest
2
+ module Resources
3
+ class Person < Harvest::HarvestResource
4
+ include Harvest::Plugins::Toggleable
5
+
6
+ # Find all entries for the given person;
7
+ # options[:from] and options[:to] are required;
8
+ # include options[:user_id] to limit by a specific project.
9
+ def entries(options={})
10
+ validate_options(options)
11
+ entry_class = Harvest::Resources::Entry.clone
12
+ entry_class.person_id = self.id
13
+ entry_class.find :all, :params => format_params(options)
14
+ end
15
+
16
+ def expenses(options={})
17
+ validate_options(options)
18
+ expense_class = Harvest::Resources::Expense.clone
19
+ expense_class.person_id = self.id
20
+ expense_class.find :all, :params => format_params(options)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_options(options)
26
+ if [:from, :to].any? {|key| !options[key].respond_to?(:strftime) }
27
+ raise ArgumentError, "Must specify :from and :to as dates."
28
+ end
29
+
30
+ if options[:from] > options[:to]
31
+ raise ArgumentError, ":start must precede :end."
32
+ end
33
+
34
+ end
35
+ def format_params(options)
36
+ ops = { :from => options[:from].strftime("%Y%m%d"),
37
+ :to => options[:to].strftime("%Y%m%d")}
38
+ ops[:project_id] = options[:project_id] if options[:project_id]
39
+ return ops
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ module Harvest
2
+ module Resources
3
+ # Supports the following:
4
+ class Project < Harvest::HarvestResource
5
+ include Harvest::Plugins::Toggleable
6
+
7
+ def users
8
+ user_class = Harvest::Resources::UserAssignment.clone
9
+ user_class.project_id = self.id
10
+ user_class
11
+ end
12
+
13
+ def tasks
14
+ task_class = Harvest::Resources::TaskAssignment.clone
15
+ task_class.project_id = self.id
16
+ task_class
17
+ end
18
+
19
+ # Find all entries for the given project;
20
+ # options[:from] and options[:to] are required;
21
+ # include options[:user_id] to limit by a specific user.
22
+ #
23
+ def entries(options={})
24
+ validate_entries_options(options)
25
+ entry_class = Harvest::Resources::Entry.clone
26
+ entry_class.project_id = self.id
27
+ entry_class.find :all, :params => format_params(options)
28
+ end
29
+
30
+ private
31
+
32
+ def validate_entries_options(options)
33
+ if [:from, :to].any? {|key| !options[key].respond_to?(:strftime) }
34
+ raise ArgumentError, "Must specify :from and :to as dates."
35
+ end
36
+
37
+ if options[:from] > options[:to]
38
+ raise ArgumentError, ":start must precede :end."
39
+ end
40
+ end
41
+
42
+ def format_params(options)
43
+ ops = { :from => options[:from].strftime("%Y%m%d"),
44
+ :to => options[:to].strftime("%Y%m%d")}
45
+ ops[:user_id] = options[:user_id] if options[:user_id]
46
+ return ops
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,8 @@
1
+ module Harvest
2
+ module Resources
3
+ class Task < Harvest::HarvestResource
4
+ include Harvest::Plugins::Toggleable
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # This class is accessed by an instance of Project.
2
+ module Harvest
3
+ module Resources
4
+ class TaskAssignment < Harvest::HarvestResource
5
+
6
+ self.element_name = "task_assignment"
7
+
8
+ class << self
9
+
10
+ def project_id=(id)
11
+ @project_id = id
12
+ set_site
13
+ end
14
+
15
+ def project_id
16
+ @project_id
17
+ end
18
+
19
+ def set_site
20
+ self.site = self.site + "/projects/#{self.project_id}"
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # This class is accessed by an instance of Project.
2
+ module Harvest
3
+ module Resources
4
+ class UserAssignment < Harvest::HarvestResource
5
+
6
+ self.element_name = "user_assignment"
7
+
8
+ class << self
9
+
10
+ def project_id=(id)
11
+ @project_id = id
12
+ set_site
13
+ end
14
+
15
+ def project_id
16
+ @project_id
17
+ end
18
+
19
+ def set_site
20
+ self.site = self.site + "/projects/#{self.project_id}"
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ class ClientIntegration < Test::Unit::TestCase
2
+
3
+ def test_should_create_and_update_a_new_client
4
+ # create
5
+ client = $harvest.clients.new
6
+ client.name = "HarvestGem, INC"
7
+ client.details = "New York, NY"
8
+ client.save
9
+ $test_client = $harvest.clients.find(:all).detect {|c| c.name == "HarvestGem, INC"}
10
+
11
+ #update
12
+ client.details = "San Francisco, CA"
13
+ client.save
14
+ assert_equal "San Francisco, CA", $harvest.clients.find($test_client.id).details
15
+ end
16
+
17
+ end
@@ -0,0 +1,11 @@
1
+ class ClientTeardown < Test::Unit::TestCase
2
+
3
+ def test_should_destroy_the_client
4
+ client = $harvest.clients.find($test_client.id)
5
+ client.destroy
6
+ assert_raise ActiveResource::ResourceNotFound do
7
+ $harvest.clients.find($test_client)
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,16 @@
1
+ class ExpenseCategoryIntegration < Test::Unit::TestCase
2
+
3
+ def test_should_create_and_update_a_new_expense_category
4
+ # create
5
+ expense_category = $harvest.expense_categories.new
6
+ expense_category.name = "GemEntertainmentBeforeUpdate"
7
+ expense_category.save
8
+ $test_expense_category = $harvest.expense_categories.find(:all).detect {|c| c.name == "GemEntertainmentBeforeUpdate"}
9
+
10
+ #update
11
+ expense_category.name = "GemEntertainment"
12
+ expense_category.save
13
+ assert_equal "GemEntertainment", $harvest.expense_categories.find(:all).detect {|c| c.name == "GemEntertainment"}.name
14
+ end
15
+
16
+ end
@@ -0,0 +1,12 @@
1
+ class ExpenseCategoryTeardown < Test::Unit::TestCase
2
+
3
+ def test_should_destroy_the_expense_category
4
+ expense_category = $harvest.expense_categories.find(:all).detect {|c| c.name == "GemEntertainment"}
5
+ expense_category.destroy
6
+ assert_raise ActiveResource::ResourceNotFound do
7
+ $harvest.expense_categories.find($test_expense_category)
8
+ end
9
+ end
10
+
11
+ end
12
+
@@ -0,0 +1,47 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ $test_mode = :integration
3
+
4
+ require File.join(File.dirname(__FILE__), "..", "test_helper")
5
+ require "test/unit/testsuite"
6
+ require "test/unit/ui/console/testrunner"
7
+
8
+ require "client_integration"
9
+ require "client_teardown"
10
+ require "expense_category_integration"
11
+ require "expense_category_teardown"
12
+ require "project_integration"
13
+ require "project_teardown"
14
+ require "task_integration"
15
+ require "task_teardown"
16
+
17
+
18
+ # Make sure that credentials have been specified:
19
+ if [:email, :password, :sub_domain].any? { |k| $integration_credentials[k].nil? }
20
+ raise ArgumentError, "Integration test cannot be run without login credentials; Please specify in test/test_helper.rb"
21
+ end
22
+
23
+ # Initialize a harvest object with credentials.
24
+ $harvest = Harvest($integration_credentials)
25
+
26
+ # Global variables to hold resources
27
+ # for later reference.
28
+ $test_client = nil
29
+ $test_project = nil
30
+ $test_person = nil
31
+
32
+ class HarvestIntegration
33
+ def self.suite
34
+ suite = Test::Unit::TestSuite.new
35
+ suite << ExpenseCategoryIntegration.suite
36
+ suite << TaskIntegration.suite
37
+ suite << ClientIntegration.suite
38
+ suite << ProjectIntegration.suite
39
+ suite << ProjectTeardown.suite
40
+ suite << ClientTeardown.suite
41
+ suite << TaskTeardown.suite
42
+ suite << ExpenseCategoryTeardown.suite
43
+ return suite
44
+ end
45
+ end
46
+
47
+ Test::Unit::UI::Console::TestRunner.run(HarvestIntegration)
@@ -0,0 +1,19 @@
1
+ class ProjectIntegration < Test::Unit::TestCase
2
+
3
+ def test_should_create_and_update_a_new_project
4
+ # create
5
+ project = $harvest.projects.new
6
+ project.name = "HarvestGem Project"
7
+ project.active = false
8
+ project.bill_by = "None"
9
+ project.client_id = $test_client.id
10
+ project.save
11
+
12
+ # update
13
+ $test_project = $harvest.projects.find(:all).detect {|c| c.name == "HarvestGem Project"}
14
+ project.active = true
15
+ project.save
16
+ assert $harvest.projects.find($test_project.id).active?
17
+ end
18
+
19
+ end
@@ -0,0 +1,12 @@
1
+ class ProjectTeardown < Test::Unit::TestCase
2
+
3
+ def test_should_destroy_the_project
4
+ project = $harvest.projects.find($test_project.id)
5
+ project.destroy
6
+ assert_raise ActiveResource::ResourceNotFound do
7
+ $harvest.projects.find($test_project)
8
+ end
9
+ end
10
+
11
+ end
12
+
@@ -0,0 +1,19 @@
1
+ class TaskIntegration < Test::Unit::TestCase
2
+
3
+ def test_should_create_and_update_a_new_task
4
+ # create
5
+ task = $harvest.tasks.new
6
+ task.billable_by_default = false
7
+ task.default_hourly_rate = 100
8
+ task.is_default = false
9
+ task.name = "GemIntegration"
10
+ task.save
11
+
12
+ # update
13
+ $test_task = $harvest.tasks.find(:all).detect {|t| t.name == "GemIntegration"}
14
+ task.name = "GemIntegrationUpdated"
15
+ task.save
16
+ assert_equal "GemIntegrationUpdated", $harvest.tasks.find($test_task.id).name
17
+ end
18
+
19
+ end
@@ -0,0 +1,12 @@
1
+ class TaskTeardown < Test::Unit::TestCase
2
+
3
+ def test_should_destroy_the_task
4
+ task = $harvest.tasks.find($test_task.id)
5
+ task.destroy
6
+ assert_raise ActiveResource::ResourceNotFound do
7
+ $harvest.tasks.find($test_task)
8
+ end
9
+ end
10
+
11
+ end
12
+
@@ -0,0 +1,51 @@
1
+ require "rubygems"
2
+ require "active_resource"
3
+ require "active_resource_throttle"
4
+ require "test/unit"
5
+ require "shoulda"
6
+ require "mocha"
7
+
8
+ # The integration test will not run by default.
9
+ # To run it, type:
10
+ # rake test:integration
11
+ #
12
+ # If running the integration test, login credentials
13
+ # must be specified here. The integration test verifies
14
+ # that various resources in a Harvest account can be created,
15
+ # deleted, updated, destroyed, etc.
16
+ #
17
+ # It's best to run this test on a trial account, where no
18
+ # sensitive data can be manipulated.
19
+ #
20
+ # Leave these values nil if not running the integration test.
21
+ $integration_credentials = {:email => nil,
22
+ :password => nil,
23
+ :sub_domain => nil}
24
+
25
+ require File.join(File.dirname(__FILE__), "..", "lib", "harvest.rb")
26
+
27
+ # Disengage throttling if in unit test mode (Integration test needs throttling).
28
+ if ($test_mode ||= :unit) == :unit
29
+ require "active_resource/http_mock"
30
+ Harvest::HarvestResource.throttle(:interval => 0, :requests => 0)
31
+ Harvest::HarvestResource.site = "http://example.com/"
32
+ end
33
+
34
+ # Load all available resources.
35
+ ResourcesPath = File.join(File.dirname(__FILE__), "..", "lib", "harvest", "resources")
36
+ Harvest.load_all_ruby_files_from_path(ResourcesPath)
37
+
38
+ # Custom assert_raise to test exception message in addition to the exception itself.
39
+ class Test::Unit::TestCase
40
+ def assert_raise_plus(exception_class, exception_message, message=nil, &block)
41
+ begin
42
+ yield
43
+ rescue => e
44
+ error_message = build_message(message, '<?>, <?> expected but was <?>, <?>', exception_class, exception_message, e.class, e.message)
45
+ assert_block(error_message) { e.class == exception_class && e.message == exception_message }
46
+ else
47
+ error_message = build_message(nil, '<?>, <?> expected but raised nothing.', exception_class, exception_message)
48
+ assert_block(error_message) { false }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,88 @@
1
+ require File.join(File.dirname(__FILE__), "..", "test_helper")
2
+
3
+ class BaseTest < Test::Unit::TestCase
4
+
5
+ context "A Harvest object" do
6
+ setup do
7
+ @password = "secret"
8
+ @email = "james@example.com"
9
+ @sub_domain = "bond"
10
+ @headers = {"User-Agent" => "HarvestGemTest"}
11
+ @harvest = Harvest::Base.new(:email => @email,
12
+ :password => @password,
13
+ :sub_domain => @sub_domain,
14
+ :headers => @headers)
15
+ end
16
+
17
+ should "initialize the resource base class" do
18
+ assert_equal "http://bond.harvestapp.com", Harvest::HarvestResource.site.to_s
19
+ assert_equal @password, Harvest::HarvestResource.password
20
+ assert_equal @email, Harvest::HarvestResource.user
21
+ end
22
+
23
+ should "raise an error if sub_domain is missing" do
24
+ assert_raise_plus ArgumentError, "Missing required option(s): sub_domain" do
25
+ Harvest(:email => "joe@example.com", :password => "secret")
26
+ end
27
+ end
28
+
29
+ should "raise an error if password is missing" do
30
+ assert_raise_plus ArgumentError, "Missing required option(s): password" do
31
+ Harvest(:email => "joe@example.com", :sub_domain => "time")
32
+ end
33
+ end
34
+
35
+ should "raise an error if email is missing" do
36
+ assert_raise_plus ArgumentError, "Missing required option(s): email" do
37
+ Harvest(:password => "secret", :sub_domain => "time")
38
+ end
39
+ end
40
+
41
+ should "set the headers" do
42
+ assert_equal @headers, Harvest::HarvestResource.headers
43
+ end
44
+
45
+ should "return the Client class" do
46
+ assert_equal Harvest::Resources::Client, @harvest.clients
47
+ end
48
+
49
+ should "return the Expense class" do
50
+ assert_equal Harvest::Resources::Expense, @harvest.expenses
51
+ end
52
+
53
+ should "return the ExpenseCategory class" do
54
+ assert_equal Harvest::Resources::ExpenseCategory, @harvest.expense_categories
55
+ end
56
+
57
+ should "return the Person class" do
58
+ assert_equal Harvest::Resources::Project, @harvest.projects
59
+ end
60
+
61
+ should "return the Project class" do
62
+ assert_equal Harvest::Resources::Project, @harvest.projects
63
+ end
64
+
65
+ should "return the Task class" do
66
+ assert_equal Harvest::Resources::Task, @harvest.tasks
67
+ end
68
+
69
+ context "with SSL enabled" do
70
+ setup do
71
+ @harvest = Harvest::Base.new(:email => @email,
72
+ :password => @password,
73
+ :sub_domain => @sub_domain,
74
+ :headers => @headers,
75
+ :ssl => true)
76
+ end
77
+
78
+ should "have https in URL" do
79
+ assert_equal "https://bond.harvestapp.com", Harvest::HarvestResource.site.to_s
80
+ end
81
+ end
82
+
83
+ context "who_am_i" do
84
+ @person = @harvest.who_am_i
85
+ end
86
+ end
87
+
88
+ end