harvest 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +3 -0
- data/LICENSE +23 -0
- data/README.rdoc +173 -0
- data/Rakefile +44 -0
- data/lib/harvest.rb +35 -0
- data/lib/harvest/base.rb +77 -0
- data/lib/harvest/harvest_resource.rb +15 -0
- data/lib/harvest/plugins/active_resource_inheritable_headers.rb +36 -0
- data/lib/harvest/plugins/toggleable.rb +12 -0
- data/lib/harvest/resources/client.rb +8 -0
- data/lib/harvest/resources/entry.rb +34 -0
- data/lib/harvest/resources/expense.rb +22 -0
- data/lib/harvest/resources/expense_category.rb +7 -0
- data/lib/harvest/resources/person.rb +44 -0
- data/lib/harvest/resources/project.rb +51 -0
- data/lib/harvest/resources/task.rb +8 -0
- data/lib/harvest/resources/task_assignment.rb +27 -0
- data/lib/harvest/resources/user_assignment.rb +27 -0
- data/test/integration/client_integration.rb +17 -0
- data/test/integration/client_teardown.rb +11 -0
- data/test/integration/expense_category_integration.rb +16 -0
- data/test/integration/expense_category_teardown.rb +12 -0
- data/test/integration/harvest_integration_test.rb +47 -0
- data/test/integration/project_integration.rb +19 -0
- data/test/integration/project_teardown.rb +12 -0
- data/test/integration/task_integration.rb +19 -0
- data/test/integration/task_teardown.rb +12 -0
- data/test/test_helper.rb +51 -0
- data/test/unit/base_test.rb +88 -0
- data/test/unit/resources/client_test.rb +82 -0
- data/test/unit/resources/expense_category_test.rb +49 -0
- data/test/unit/resources/expense_test.rb +14 -0
- data/test/unit/resources/person_test.rb +150 -0
- data/test/unit/resources/project_test.rb +154 -0
- data/test/unit/resources/task_assignment_test.rb +72 -0
- data/test/unit/resources/task_test.rb +82 -0
- data/test/unit/resources/user_assignment_test.rb +71 -0
- 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,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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|