hcl 0.4.9 → 0.4.10
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.
- checksums.yaml +4 -4
- data/CHANGELOG +5 -0
- data/Gemfile +1 -0
- data/README.markdown +36 -6
- data/lib/hcl.rb +3 -0
- data/lib/hcl/app.rb +11 -18
- data/lib/hcl/commands.rb +26 -15
- data/lib/hcl/console.rb +22 -0
- data/lib/hcl/day_entry.rb +17 -36
- data/lib/hcl/harvest_middleware.rb +58 -0
- data/lib/hcl/net.rb +39 -0
- data/lib/hcl/project.rb +1 -0
- data/lib/hcl/task.rb +15 -23
- data/lib/hcl/timesheet_resource.rb +46 -79
- data/lib/hcl/version.rb +1 -1
- data/test/app_test.rb +5 -4
- data/test/command_test.rb +11 -10
- data/test/day_entry_test.rb +47 -34
- data/test/ext/capture_output.rb +4 -2
- data/test/net_test.rb +40 -0
- data/test/task_test.rb +38 -21
- data/test/test_helper.rb +22 -6
- metadata +62 -3
- data/test/timesheet_resource_test.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e94f259c4106d6c9fd541aea356ad7bcc14c8b9
|
4
|
+
data.tar.gz: 6c62d7d40bd19500585b8bfd8f2c1ef70a6861c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fbad2e2f47d19d6c4bed45cfc0ee74be7abbdfbaca0289891ed5bcc04a5ac55b51f2ea612575fc43b9006cb494303d21ac29252b427073b5b9a7470cd4c40b7
|
7
|
+
data.tar.gz: 0f98b72e9ec279307e13ee2edf6093725ea1ebd8d788201dab4f17bc00df3ff9da97a2a1ab9fff8909b9ecae287ee9ea1125cdf087b935028efa4df7247d2782
|
data/CHANGELOG
CHANGED
data/Gemfile
CHANGED
data/README.markdown
CHANGED
@@ -3,11 +3,14 @@
|
|
3
3
|
HCl is a command-line tool for interacting with Harvest time sheets using the
|
4
4
|
[Harvest time tracking API][htt].
|
5
5
|
|
6
|
-
[
|
6
|
+
[View this documentation online][rdoc].
|
7
7
|
|
8
8
|
[](https://travis-ci.org/zenhob/hcl)
|
9
9
|
[](http://badge.fury.io/rb/hcl)
|
10
10
|
|
11
|
+
[htt]: http://www.getharvest.com/api/time_tracking
|
12
|
+
[rdoc]: http://rdoc.info/github/zenhob/hcl/file/README.markdown
|
13
|
+
|
11
14
|
## Quick Start
|
12
15
|
|
13
16
|
You can install hcl directly from rubygems.org:
|
@@ -30,6 +33,7 @@ or you can install from source:
|
|
30
33
|
hcl alias <task_alias> <project_id> <task_id>
|
31
34
|
hcl aliases
|
32
35
|
hcl (cancel | nvm | oops)
|
36
|
+
hcl config
|
33
37
|
|
34
38
|
### Available Projects and Tasks
|
35
39
|
|
@@ -117,16 +121,42 @@ configuration:
|
|
117
121
|
|
118
122
|
### Configuration Profiles
|
119
123
|
|
120
|
-
You can
|
121
|
-
`
|
124
|
+
You can modify your credentials with the `--reauth` option, and review them
|
125
|
+
with `hcl config`. If you'd rather store multiple configurations at
|
126
|
+
once, specify an alternate configuration directory in the environment as
|
127
|
+
`HCL_DIR`. This can be used to interact with multiple harvest accounts at
|
128
|
+
once.
|
129
|
+
|
122
130
|
Here is a shell alias `myhcl` with a separate configuration from the
|
123
|
-
main `hcl` command,
|
131
|
+
main `hcl` command, and another command to configure alias completion:
|
124
132
|
|
125
133
|
alias myhcl="env HCL_DIR=~/.myhcl hcl"
|
126
134
|
eval `myhcl completion myhcl`
|
127
135
|
|
128
|
-
|
129
|
-
`
|
136
|
+
Adding something like the above to your bashrc will enable a new command,
|
137
|
+
`myhcl`. When using `myhcl` you can use different credentials and aliases,
|
138
|
+
while `hcl` will continue to function with your original configuration.
|
139
|
+
|
140
|
+
### Interactive Console
|
141
|
+
|
142
|
+
An interactive Ruby console is provided to allow you to use the fairly
|
143
|
+
powerful Harvest API client built into HCl, since not all of its
|
144
|
+
features are exposed via the command line. The current {HCl::App}
|
145
|
+
instance is available as `hcl`.
|
146
|
+
|
147
|
+
It's also possible to issue HCl commands directly (except `alias`, see
|
148
|
+
below), or to query specific JSON end points and have the results
|
149
|
+
pretty-printed:
|
150
|
+
|
151
|
+
hcl console
|
152
|
+
hcl> show "yesterday"
|
153
|
+
# => prints yesterday's timesheet, note the quotes!
|
154
|
+
hcl> hcl.http.get('daily')
|
155
|
+
# => displays a pretty-printed version of the JSON output
|
156
|
+
|
157
|
+
Note that the HCl internals may change without notice.
|
158
|
+
Also, commands (like `alias`) that are also reserved words in Ruby
|
159
|
+
can't be issued directly (use `send :alias` instead).
|
130
160
|
|
131
161
|
### Date Formats
|
132
162
|
|
data/lib/hcl.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
module HCl
|
2
2
|
autoload :VERSION, 'hcl/version'
|
3
3
|
autoload :App, 'hcl/app'
|
4
|
+
autoload :Net, 'hcl/net'
|
4
5
|
autoload :Commands, 'hcl/commands'
|
6
|
+
autoload :Console, 'hcl/console'
|
5
7
|
autoload :TimesheetResource, 'hcl/timesheet_resource'
|
6
8
|
autoload :Utility, 'hcl/utility'
|
7
9
|
autoload :Project, 'hcl/project'
|
8
10
|
autoload :Task, 'hcl/task'
|
9
11
|
autoload :DayEntry, 'hcl/day_entry'
|
12
|
+
autoload :HarvestMiddleware, 'hcl/harvest_middleware'
|
10
13
|
end
|
data/lib/hcl/app.rb
CHANGED
@@ -9,11 +9,11 @@ module HCl
|
|
9
9
|
include HCl::Utility
|
10
10
|
include HCl::Commands
|
11
11
|
|
12
|
-
HCL_DIR = ENV['HCL_DIR'] || "#{ENV['HOME']}/.hcl"
|
13
|
-
SETTINGS_FILE = "#{HCL_DIR}/settings.yml"
|
14
|
-
CONFIG_FILE = "#{HCL_DIR}/config.yml"
|
15
|
-
|
16
|
-
|
12
|
+
HCL_DIR = (ENV['HCL_DIR'] || "#{ENV['HOME']}/.hcl").freeze
|
13
|
+
SETTINGS_FILE = "#{HCL_DIR}/settings.yml".freeze
|
14
|
+
CONFIG_FILE = "#{HCL_DIR}/config.yml".freeze
|
15
|
+
|
16
|
+
attr_reader :http
|
17
17
|
|
18
18
|
def initialize
|
19
19
|
FileUtils.mkdir_p(HCL_DIR)
|
@@ -64,15 +64,15 @@ module HCl
|
|
64
64
|
rescue SocketError => e
|
65
65
|
$stderr.puts "Connection failed. (#{e.message})"
|
66
66
|
exit 1
|
67
|
-
rescue
|
67
|
+
rescue HarvestMiddleware::ThrottleFailure => e
|
68
68
|
$stderr.puts "Too many requests, retrying in #{e.retry_after+5} seconds..."
|
69
69
|
sleep e.retry_after+5
|
70
70
|
run
|
71
|
-
rescue
|
71
|
+
rescue HarvestMiddleware::AuthFailure => e
|
72
72
|
$stderr.puts "Unable to authenticate: #{e}"
|
73
73
|
request_config
|
74
74
|
run
|
75
|
-
rescue
|
75
|
+
rescue HarvestMiddleware::Failure => e
|
76
76
|
$stderr.puts "API failure: #{e}"
|
77
77
|
exit 1
|
78
78
|
end
|
@@ -142,11 +142,7 @@ EOM
|
|
142
142
|
if has_security_command?
|
143
143
|
load_password config
|
144
144
|
end
|
145
|
-
|
146
|
-
elsif File.exists? OLD_CONFIG_FILE
|
147
|
-
config = YAML::load File.read(OLD_CONFIG_FILE)
|
148
|
-
TimesheetResource.configure config
|
149
|
-
write_config config
|
145
|
+
@http = HCl::Net.new config
|
150
146
|
else
|
151
147
|
request_config
|
152
148
|
end
|
@@ -158,8 +154,8 @@ EOM
|
|
158
154
|
config['login'] = ask("Email Address: ").to_s
|
159
155
|
config['password'] = ask("Password: ") { |q| q.echo = false }.to_s
|
160
156
|
config['subdomain'] = ask("Subdomain: ").to_s
|
161
|
-
config['ssl'] =
|
162
|
-
|
157
|
+
config['ssl'] = /^y/.match(ask("Use SSL? (y/n): ").downcase)
|
158
|
+
@http = HCl::Net.new config
|
163
159
|
write_config config
|
164
160
|
end
|
165
161
|
|
@@ -177,9 +173,6 @@ EOM
|
|
177
173
|
def read_settings
|
178
174
|
if File.exists? SETTINGS_FILE
|
179
175
|
@settings = YAML.load(File.read(SETTINGS_FILE))
|
180
|
-
elsif File.exists? OLD_SETTINGS_FILE
|
181
|
-
@settings = YAML.load(File.read(OLD_SETTINGS_FILE))
|
182
|
-
write_settings
|
183
176
|
else
|
184
177
|
@settings = {}
|
185
178
|
end
|
data/lib/hcl/commands.rb
CHANGED
@@ -5,10 +5,20 @@ module HCl
|
|
5
5
|
module Commands
|
6
6
|
class Error < StandardError; end
|
7
7
|
|
8
|
+
# Display a sanitized view of your auth credentials.
|
9
|
+
def config
|
10
|
+
http.config_hash.merge(password:'***').map {|k,v| "#{k}: #{v}" }.join("\n")
|
11
|
+
end
|
12
|
+
|
13
|
+
def console
|
14
|
+
Console.new(self)
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
8
18
|
def tasks project_code=nil
|
9
19
|
tasks = Task.all
|
10
20
|
if tasks.empty? # cache tasks
|
11
|
-
DayEntry.
|
21
|
+
DayEntry.today
|
12
22
|
tasks = Task.all
|
13
23
|
end
|
14
24
|
tasks.select! {|t| t.project.code == project_code } if project_code
|
@@ -33,9 +43,9 @@ module HCl
|
|
33
43
|
end
|
34
44
|
|
35
45
|
def cancel
|
36
|
-
entry = DayEntry.with_timer || DayEntry.last
|
46
|
+
entry = DayEntry.with_timer(http) || DayEntry.last(http)
|
37
47
|
if entry
|
38
|
-
if entry.cancel
|
48
|
+
if entry.cancel http
|
39
49
|
"Deleted entry #{entry}."
|
40
50
|
else
|
41
51
|
fail "Failed to delete #{entry}!"
|
@@ -67,7 +77,8 @@ module HCl
|
|
67
77
|
end
|
68
78
|
end
|
69
79
|
|
70
|
-
def completion command
|
80
|
+
def completion command=nil
|
81
|
+
command ||= $PROGRAM_NAME.split('/').last
|
71
82
|
%[complete -W "#{aliases.join ' '}" #{command}]
|
72
83
|
end
|
73
84
|
|
@@ -81,23 +92,23 @@ module HCl
|
|
81
92
|
if task.nil?
|
82
93
|
fail "Unknown task alias, try one of the following: ", aliases.join(', ')
|
83
94
|
end
|
84
|
-
timer = task.start
|
95
|
+
timer = task.start http,
|
85
96
|
:starting_time => starting_time,
|
86
97
|
:note => args.join(' ')
|
87
98
|
"Started timer for #{timer} (at #{current_time})"
|
88
99
|
end
|
89
100
|
|
90
101
|
def log *args
|
91
|
-
fail "There is already a timer running." if DayEntry.with_timer
|
102
|
+
fail "There is already a timer running." if DayEntry.with_timer(http)
|
92
103
|
start *args
|
93
104
|
stop
|
94
105
|
end
|
95
106
|
|
96
107
|
def stop *args
|
97
|
-
entry = DayEntry.with_timer || DayEntry.with_timer(DateTime.yesterday)
|
108
|
+
entry = DayEntry.with_timer(http) || DayEntry.with_timer(http, DateTime.yesterday)
|
98
109
|
if entry
|
99
|
-
entry.append_note(args.join(' ')) if args.any?
|
100
|
-
entry.toggle
|
110
|
+
entry.append_note(http, args.join(' ')) if args.any?
|
111
|
+
entry.toggle http
|
101
112
|
"Stopped #{entry} (at #{current_time})"
|
102
113
|
else
|
103
114
|
fail "No running timers found."
|
@@ -105,12 +116,12 @@ module HCl
|
|
105
116
|
end
|
106
117
|
|
107
118
|
def note *args
|
108
|
-
entry = DayEntry.with_timer
|
119
|
+
entry = DayEntry.with_timer http
|
109
120
|
if entry
|
110
121
|
if args.empty?
|
111
122
|
return entry.notes
|
112
123
|
else
|
113
|
-
entry.append_note args.join(' ')
|
124
|
+
entry.append_note http, args.join(' ')
|
114
125
|
"Added note to #{entry}."
|
115
126
|
end
|
116
127
|
else
|
@@ -122,7 +133,7 @@ module HCl
|
|
122
133
|
date = args.empty? ? nil : Chronic.parse(args.join(' '))
|
123
134
|
total_hours = 0.0
|
124
135
|
result = ''
|
125
|
-
DayEntry.
|
136
|
+
DayEntry.daily(http, date).each do |day|
|
126
137
|
running = day.running? ? '(running) ' : ''
|
127
138
|
columns = HighLine::SystemExtensions.terminal_size[0] rescue 80
|
128
139
|
result << "\t#{day.formatted_hours}\t#{running}#{day.project}: #{day.notes.lines.to_a.last}\n"[0..columns-1]
|
@@ -136,12 +147,12 @@ module HCl
|
|
136
147
|
ident = get_ident args
|
137
148
|
entry = if ident
|
138
149
|
task_ids = get_task_ids ident, args
|
139
|
-
DayEntry.last_by_task *task_ids
|
150
|
+
DayEntry.last_by_task http, *task_ids
|
140
151
|
else
|
141
|
-
DayEntry.last
|
152
|
+
DayEntry.last(http)
|
142
153
|
end
|
143
154
|
if entry
|
144
|
-
entry.toggle
|
155
|
+
entry.toggle http
|
145
156
|
else
|
146
157
|
fail "No matching timer found."
|
147
158
|
end
|
data/lib/hcl/console.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
module HCl
|
5
|
+
class Console
|
6
|
+
attr_reader :hcl
|
7
|
+
def initialize app
|
8
|
+
@hcl = app
|
9
|
+
prompt = $PROGRAM_NAME.split('/').last + "> "
|
10
|
+
columns = HighLine::SystemExtensions.terminal_size[0] rescue 80
|
11
|
+
binding.pry quiet: true,
|
12
|
+
prompt:[->(a,b,c){ prompt }],
|
13
|
+
print:->(io, *p){ PP.pp p, io, columns }
|
14
|
+
end
|
15
|
+
|
16
|
+
Commands.instance_methods.each do |command|
|
17
|
+
define_method command do |*args|
|
18
|
+
puts @hcl.send(command, *args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/hcl/day_entry.rb
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
-
require 'rexml/document'
|
2
|
-
|
3
1
|
module HCl
|
4
2
|
class DayEntry < TimesheetResource
|
5
3
|
include Utility
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
from_xml get(url)
|
12
|
-
end
|
5
|
+
collection_name :day_entries
|
6
|
+
resources :today, 'daily', load_cb:->(data) { Task.cache_tasks_hash data }
|
7
|
+
resources(:daily) {|date| date ? "daily/#{date.strftime '%j/%Y'}" : 'daily' }
|
8
|
+
resource(:project_info, class_name:'Project') { "projects/#{project_id}" }
|
13
9
|
|
14
10
|
def to_s
|
15
11
|
"#{client} - #{project} - #{task} (#{formatted_hours})"
|
@@ -19,19 +15,10 @@ module HCl
|
|
19
15
|
@data[:task]
|
20
16
|
end
|
21
17
|
|
22
|
-
def
|
23
|
-
doc = REXML::Document.new xml
|
24
|
-
raise Failure, "No root node in XML document: #{xml}" if doc.root.nil?
|
25
|
-
Task.cache_tasks doc
|
26
|
-
doc.root.elements.collect('//day_entry') do |day|
|
27
|
-
new xml_to_hash(day)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def cancel
|
18
|
+
def cancel http
|
32
19
|
begin
|
33
|
-
|
34
|
-
rescue
|
20
|
+
http.delete("daily/delete/#{id}")
|
21
|
+
rescue HarvestMiddleware::Failure
|
35
22
|
return false
|
36
23
|
end
|
37
24
|
true
|
@@ -42,38 +29,32 @@ module HCl
|
|
42
29
|
end
|
43
30
|
|
44
31
|
# Append a string to the notes for this task.
|
45
|
-
def append_note new_notes
|
32
|
+
def append_note http, new_notes
|
46
33
|
# If I don't include hours it gets reset.
|
47
34
|
# This doens't appear to be the case for task and project.
|
48
35
|
(self.notes << "\n#{new_notes}").lstrip!
|
49
|
-
|
50
|
-
%{<request><notes>#{notes}</notes><hours>#{hours}</hours></request>}
|
36
|
+
http.post "daily/update/#{id}", notes:notes, hours:hours
|
51
37
|
end
|
52
38
|
|
53
|
-
def self.with_timer date=nil
|
54
|
-
|
39
|
+
def self.with_timer http, date=nil
|
40
|
+
daily(http, date).detect {|t| t.running? }
|
55
41
|
end
|
56
42
|
|
57
|
-
def self.last_by_task project_id, task_id
|
58
|
-
|
43
|
+
def self.last_by_task http, project_id, task_id
|
44
|
+
today(http).sort {|a,b| b.updated_at<=>a.updated_at}.
|
59
45
|
detect {|t| t.project_id == project_id && t.task_id == task_id }
|
60
46
|
end
|
61
47
|
|
62
|
-
def self.last
|
63
|
-
|
48
|
+
def self.last http
|
49
|
+
today(http).sort {|a,b| a.updated_at<=>b.updated_at}[-1]
|
64
50
|
end
|
65
51
|
|
66
52
|
def running?
|
67
53
|
!@data[:timer_started_at].nil? && !@data[:timer_started_at].empty?
|
68
54
|
end
|
69
55
|
|
70
|
-
def
|
71
|
-
|
72
|
-
# TODO cache client/project names and ids
|
73
|
-
end
|
74
|
-
|
75
|
-
def toggle
|
76
|
-
DayEntry.get("daily/timer/#{id}")
|
56
|
+
def toggle http
|
57
|
+
http.get("daily/timer/#{id}")
|
77
58
|
self
|
78
59
|
end
|
79
60
|
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
class HCl::HarvestMiddleware < Faraday::Request::BasicAuthentication
|
4
|
+
Faraday.register_middleware harvest: ->{ self }
|
5
|
+
MIME_TYPE = 'application/json'.freeze
|
6
|
+
|
7
|
+
dependency do
|
8
|
+
require 'yajl'
|
9
|
+
require 'escape_utils'
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
# encode with and accept json
|
14
|
+
env[:request_headers]['Accept'] = MIME_TYPE
|
15
|
+
env[:request_headers]['Content-Type'] = MIME_TYPE
|
16
|
+
env[:body] = Yajl::Encoder.encode(env[:body])
|
17
|
+
|
18
|
+
# response processing
|
19
|
+
super(env).on_complete do |env|
|
20
|
+
case env[:status]
|
21
|
+
when 200..299
|
22
|
+
begin
|
23
|
+
env[:body] = deep_html_unescape(Yajl::Parser.parse(env[:body], symbolize_keys:true))
|
24
|
+
rescue Yajl::ParseError
|
25
|
+
env[:body]
|
26
|
+
end
|
27
|
+
when 300..399
|
28
|
+
raise Failure, "Redirected! Perhaps your ssl configuration variable is set incorrectly?"
|
29
|
+
when 400..499
|
30
|
+
raise AuthFailure, "Login failed."
|
31
|
+
when 503
|
32
|
+
raise ThrottleFailure, env
|
33
|
+
else
|
34
|
+
raise Failure, "Unexpected response from the upstream API."
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def deep_html_unescape obj
|
40
|
+
if obj.kind_of? Hash
|
41
|
+
obj.inject({}){|o,(k,v)| o.update(k => deep_html_unescape(v)) }
|
42
|
+
elsif obj.kind_of? Array
|
43
|
+
obj.inject([]){|o,v| o << deep_html_unescape(v) }
|
44
|
+
else
|
45
|
+
EscapeUtils.unescape_html(obj.to_s)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Failure < StandardError; end
|
50
|
+
class AuthFailure < StandardError; end
|
51
|
+
class ThrottleFailure < StandardError
|
52
|
+
attr_reader :retry_after
|
53
|
+
def initialize env
|
54
|
+
@retry_after = env[:response_headers]['retry-after'].to_i
|
55
|
+
super "Too many requests! Try again in #{@retry_after} seconds."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/hcl/net.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'hcl/harvest_middleware'
|
2
|
+
require 'faraday'
|
3
|
+
|
4
|
+
module HCl
|
5
|
+
class Net
|
6
|
+
# configuration accessors
|
7
|
+
CONFIG_VARS = [ :login, :password, :subdomain, :ssl ].freeze.
|
8
|
+
each { |config_var| attr_reader config_var }
|
9
|
+
|
10
|
+
def config_hash
|
11
|
+
CONFIG_VARS.inject({}) {|c,k| c.update(k => send(k)) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize opts
|
15
|
+
@login = opts['login'].freeze
|
16
|
+
@password = opts['password'].freeze
|
17
|
+
@subdomain = opts['subdomain'].freeze
|
18
|
+
@ssl = !!opts['ssl']
|
19
|
+
@http = Faraday.new(
|
20
|
+
"http#{ssl ? 's' : '' }://#{subdomain}.harvestapp.com"
|
21
|
+
) do |f|
|
22
|
+
f.use :harvest, login, password
|
23
|
+
f.adapter Faraday.default_adapter
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def get action
|
28
|
+
@http.get(action).body
|
29
|
+
end
|
30
|
+
|
31
|
+
def post action, data
|
32
|
+
@http.post(action, data).body
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete action
|
36
|
+
@http.delete(action).body
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/hcl/project.rb
CHANGED
data/lib/hcl/task.rb
CHANGED
@@ -3,18 +3,13 @@ require 'fileutils'
|
|
3
3
|
|
4
4
|
module HCl
|
5
5
|
class Task < TimesheetResource
|
6
|
-
def self.
|
7
|
-
tasks = []
|
8
|
-
|
9
|
-
project = Project.new xml_to_hash(project_elem)
|
10
|
-
tasks.concat(project_elem.elements.collect('tasks/task') do |task|
|
11
|
-
new xml_to_hash(task).merge(:project => project)
|
12
|
-
end)
|
13
|
-
end
|
6
|
+
def self.cache_tasks_hash day_entry_hash
|
7
|
+
tasks = day_entry_hash[:projects].
|
8
|
+
map { |p| p[:tasks].map {|t| new t.merge(project:Project.new(p)) } }.flatten.uniq
|
14
9
|
unless tasks.empty?
|
15
10
|
FileUtils.mkdir_p(cache_dir)
|
16
11
|
File.open(cache_file, 'w') do |f|
|
17
|
-
f.write tasks.
|
12
|
+
f.write tasks.to_yaml
|
18
13
|
end
|
19
14
|
end
|
20
15
|
end
|
@@ -52,27 +47,24 @@ module HCl
|
|
52
47
|
end
|
53
48
|
end
|
54
49
|
|
55
|
-
def add opts
|
50
|
+
def add http, opts
|
56
51
|
notes = opts[:note]
|
57
52
|
starting_time = opts[:starting_time] || 0
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
</request>
|
66
|
-
EOT
|
67
|
-
days.first
|
53
|
+
DayEntry.new http.post("daily/add", {
|
54
|
+
notes: notes,
|
55
|
+
hours: starting_time,
|
56
|
+
project_id: project.id,
|
57
|
+
task_id: id,
|
58
|
+
spent_at: Date.today
|
59
|
+
})
|
68
60
|
end
|
69
61
|
|
70
|
-
def start opts
|
71
|
-
day = add opts
|
62
|
+
def start http, opts
|
63
|
+
day = add http, opts
|
72
64
|
if day.running?
|
73
65
|
day
|
74
66
|
else
|
75
|
-
DayEntry.
|
67
|
+
DayEntry.new http.get("daily/timer/#{day.id}")
|
76
68
|
end
|
77
69
|
end
|
78
70
|
end
|
@@ -1,84 +1,9 @@
|
|
1
|
-
require 'net/http'
|
2
|
-
require 'net/https'
|
3
|
-
require 'cgi'
|
4
|
-
|
5
1
|
module HCl
|
6
2
|
class TimesheetResource
|
7
|
-
class Failure < StandardError; end
|
8
|
-
class AuthFailure < StandardError; end
|
9
|
-
class ThrottleFailure < StandardError
|
10
|
-
attr_reader :retry_after
|
11
|
-
def initialize response
|
12
|
-
@retry_after = response.headers['Retry-After'].to_i
|
13
|
-
super "Too many requests! Try again in #{@retry_after} seconds."
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.configure opts = nil
|
18
|
-
if opts
|
19
|
-
self.login = opts['login']
|
20
|
-
self.password = opts['password']
|
21
|
-
self.subdomain = opts['subdomain']
|
22
|
-
self.ssl = opts['ssl']
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# configuration accessors
|
27
|
-
%w[ login password subdomain ssl ].each do |config_var|
|
28
|
-
class_eval <<-EOC
|
29
|
-
def self.#{config_var}= arg
|
30
|
-
@@#{config_var} = arg
|
31
|
-
end
|
32
|
-
def self.#{config_var}
|
33
|
-
@@#{config_var}
|
34
|
-
end
|
35
|
-
EOC
|
36
|
-
end
|
37
|
-
|
38
3
|
def initialize params
|
39
4
|
@data = params
|
40
5
|
end
|
41
6
|
|
42
|
-
def self.get action
|
43
|
-
http_do Net::HTTP::Get, action
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.post action, data
|
47
|
-
http_do Net::HTTP::Post, action, data
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.delete action
|
51
|
-
http_do Net::HTTP::Delete, action
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.connect
|
55
|
-
Net::HTTP.new("#{subdomain}.harvestapp.com", (ssl ? 443 : 80)).tap do |https|
|
56
|
-
https.use_ssl = ssl
|
57
|
-
https.verify_mode = OpenSSL::SSL::VERIFY_NONE if ssl
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def self.http_do method_class, action, data = nil
|
62
|
-
https = connect
|
63
|
-
request = method_class.new "/#{action}"
|
64
|
-
request.basic_auth login, password
|
65
|
-
request.content_type = 'application/xml'
|
66
|
-
request['Accept'] = 'application/xml'
|
67
|
-
response = https.request request, data
|
68
|
-
case response
|
69
|
-
when Net::HTTPSuccess
|
70
|
-
response.body
|
71
|
-
when Net::HTTPFound
|
72
|
-
raise Failure, "Redirected! Perhaps your ssl configuration variable is set incorrectly?"
|
73
|
-
when Net::HTTPServiceUnavailable
|
74
|
-
raise ThrottleFailure, response
|
75
|
-
when Net::HTTPUnauthorized
|
76
|
-
raise AuthFailure, "Login failed."
|
77
|
-
else
|
78
|
-
raise Failure, "Unexpected response from the upstream API."
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
7
|
def id
|
83
8
|
@data[:id]
|
84
9
|
end
|
@@ -91,10 +16,52 @@ module HCl
|
|
91
16
|
(@data && @data.key?(method.to_sym)) || super
|
92
17
|
end
|
93
18
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
19
|
+
class << self
|
20
|
+
def _prepare_resource name, *args, &url_cb
|
21
|
+
((@resources ||= {})[name] = {}).tap do |res|
|
22
|
+
opt_or_cb = args.pop
|
23
|
+
res[:url_cb] = url_cb
|
24
|
+
res[:opts] = {}
|
25
|
+
case opt_or_cb
|
26
|
+
when String
|
27
|
+
res[:url_cb] = ->() { opt_or_cb }
|
28
|
+
res[:opts] = args.pop || {}
|
29
|
+
when Hash
|
30
|
+
res[:opts] = opt_or_cb
|
31
|
+
url = args.pop
|
32
|
+
res[:url_cb] = ->() { url } if url
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def resources name, *args, &url_cb
|
38
|
+
res = _prepare_resource name, *args, &url_cb
|
39
|
+
cls = res[:opts][:class_name] ? HCl.const_get(res[:opts][:class_name]) : self
|
40
|
+
method = cls == self ? :define_singleton_method : :define_method
|
41
|
+
send(method, name) do |http, *args|
|
42
|
+
url = instance_exec *args, &res[:url_cb]
|
43
|
+
cb = res[:opts][:load_cb]
|
44
|
+
http.get(url).tap{|e| cb.call(e) if cb }[cls.collection_name].map{|e|new(e)}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def resource name, *args, &url_cb
|
49
|
+
res = _prepare_resource name, *args, &url_cb
|
50
|
+
cls = res[:opts][:class_name] ? HCl.const_get(res[:opts][:class_name]) : self
|
51
|
+
method = cls == self ? :define_singleton_method : :define_method
|
52
|
+
send(method, name) do |http, *args|
|
53
|
+
url = instance_exec *args, &res[:url_cb]
|
54
|
+
cb = res[:opts][:load_cb]
|
55
|
+
cls.new http.get(url).tap{|e| cb.call(e) if cb }[cls.underscore_name]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def underscore_name
|
60
|
+
@underscore_name ||= name.split('::').last.split(/(?=[A-Z])/).map(&:downcase).join('_').to_sym
|
61
|
+
end
|
62
|
+
|
63
|
+
def collection_name name=nil
|
64
|
+
name ? (@collection_name = name) : @collection_name
|
98
65
|
end
|
99
66
|
end
|
100
67
|
end
|
data/lib/hcl/version.rb
CHANGED
data/test/app_test.rb
CHANGED
@@ -2,6 +2,7 @@ require 'test_helper'
|
|
2
2
|
class AppTest < HCl::TestCase
|
3
3
|
|
4
4
|
def setup
|
5
|
+
super
|
5
6
|
# touch config to avoid triggering manual config
|
6
7
|
FileUtils.mkdir_p HCl::App::HCL_DIR
|
7
8
|
FileUtils.touch File.join(HCl::App::HCL_DIR, "config.yml")
|
@@ -13,7 +14,7 @@ class AppTest < HCl::TestCase
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def test_command_show
|
16
|
-
HCl::DayEntry.expects(:
|
17
|
+
HCl::DayEntry.expects(:daily).returns [HCl::DayEntry.new(
|
17
18
|
hours:'2.06', notes:'hi world', project:'App'
|
18
19
|
)]
|
19
20
|
HCl::App.command 'show'
|
@@ -23,7 +24,7 @@ class AppTest < HCl::TestCase
|
|
23
24
|
app = HCl::App.new
|
24
25
|
throttled = states('throttled').starts_as(false)
|
25
26
|
app.expects(:show).
|
26
|
-
raises(HCl::
|
27
|
+
raises(HCl::HarvestMiddleware::ThrottleFailure, {response_headers:{'retry-after' => 42}}).
|
27
28
|
then(throttled.is(true))
|
28
29
|
app.expects(:sleep).with(47).when(throttled.is(true))
|
29
30
|
app.expects(:show).when(throttled.is(true))
|
@@ -48,7 +49,7 @@ class AppTest < HCl::TestCase
|
|
48
49
|
def test_configure_on_auth_failure
|
49
50
|
app = HCl::App.new
|
50
51
|
configured = states('configured').starts_as(false)
|
51
|
-
app.expects(:show).raises(HCl::
|
52
|
+
app.expects(:show).raises(HCl::HarvestMiddleware::AuthFailure).when(configured.is(false))
|
52
53
|
app.expects(:ask).returns('xxx').times(4).when(configured.is(false))
|
53
54
|
app.expects(:write_config).then(configured.is(true))
|
54
55
|
app.expects(:show).when(configured.is(true))
|
@@ -58,7 +59,7 @@ class AppTest < HCl::TestCase
|
|
58
59
|
|
59
60
|
def test_api_failure
|
60
61
|
app = HCl::App.new
|
61
|
-
app.expects(:show).raises(HCl::
|
62
|
+
app.expects(:show).raises(HCl::HarvestMiddleware::Failure)
|
62
63
|
app.expects(:exit).with(1)
|
63
64
|
app.process_args('show').run
|
64
65
|
assert_match /API failure/i, error_output
|
data/test/command_test.rb
CHANGED
@@ -4,6 +4,7 @@ class CommandTest < HCl::TestCase
|
|
4
4
|
include HCl::Utility
|
5
5
|
|
6
6
|
def setup
|
7
|
+
super
|
7
8
|
@settings = {}
|
8
9
|
end
|
9
10
|
|
@@ -34,7 +35,7 @@ class CommandTest < HCl::TestCase
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def test_show
|
37
|
-
HCl::DayEntry.expects(:
|
38
|
+
HCl::DayEntry.expects(:daily).returns([HCl::DayEntry.new({
|
38
39
|
hours:'2.06',
|
39
40
|
notes: 'hi world',
|
40
41
|
project: 'App'
|
@@ -68,15 +69,15 @@ class CommandTest < HCl::TestCase
|
|
68
69
|
project: HCl::Project.new(id:456, name:'App', client:'Bob', code:'b')
|
69
70
|
)
|
70
71
|
HCl::Task.expects(:find).with('456','123').returns(task)
|
71
|
-
task.expects(:start).with(starting_time:nil, note:'do stuff')
|
72
|
+
task.expects(:start).with(http, starting_time:nil, note:'do stuff')
|
72
73
|
start *%w[ 456 123 do stuff ]
|
73
74
|
end
|
74
75
|
|
75
76
|
def test_stop
|
76
77
|
entry = stub
|
77
|
-
|
78
|
-
|
79
|
-
|
78
|
+
register_uri(:get, '/daily', {day_entries:[{id:123,notes:'',hours:1,client:nil,project:nil,timer_started_at:DateTime.now}]})
|
79
|
+
register_uri(:post, '/daily/update/123', {day_entry:{notes:'all done'}})
|
80
|
+
register_uri(:get, '/daily/timer/123')
|
80
81
|
stop 'all done'
|
81
82
|
end
|
82
83
|
|
@@ -90,22 +91,22 @@ class CommandTest < HCl::TestCase
|
|
90
91
|
def test_resume_with_task_alias
|
91
92
|
entry = stub
|
92
93
|
expects(:get_task_ids).with('mytask',[]).returns(%w[ 456 789 ])
|
93
|
-
HCl::DayEntry.expects(:last_by_task).with('456', '789').returns(entry)
|
94
|
-
entry.expects(:toggle)
|
94
|
+
HCl::DayEntry.expects(:last_by_task).with(http, '456', '789').returns(entry)
|
95
|
+
entry.expects(:toggle).with(http)
|
95
96
|
resume 'mytask'
|
96
97
|
end
|
97
98
|
|
98
99
|
def test_cancel
|
99
100
|
entry = stub
|
100
|
-
HCl::DayEntry.expects(:with_timer).returns(entry)
|
101
|
-
entry.expects(:cancel).returns(true)
|
101
|
+
HCl::DayEntry.expects(:with_timer).with(http).returns(entry)
|
102
|
+
entry.expects(:cancel).with(http).returns(true)
|
102
103
|
cancel
|
103
104
|
end
|
104
105
|
|
105
106
|
def test_note
|
106
107
|
entry = stub
|
107
108
|
HCl::DayEntry.expects(:with_timer).returns(entry)
|
108
|
-
entry.expects(:append_note).with('hi world')
|
109
|
+
entry.expects(:append_note).with(http, 'hi world')
|
109
110
|
note 'hi world'
|
110
111
|
end
|
111
112
|
|
data/test/day_entry_test.rb
CHANGED
@@ -1,49 +1,62 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
class DayEntryTest < HCl::TestCase
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
4
|
+
def test_project_info
|
5
|
+
register_uri(:get, '/daily', {projects:[], day_entries:[{project_id:123}]})
|
6
|
+
register_uri(:get, '/projects/123', {project:{name:'fun times'}})
|
7
|
+
assert_equal 'fun times', HCl::DayEntry.today(http).first.project_info(http).name
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_all_today_empty
|
11
|
+
register_uri(:get, '/daily', {projects:[],day_entries:[]})
|
12
|
+
assert HCl::DayEntry.today(http).empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_all_today
|
16
|
+
register_uri(:get, '/daily', {projects:[], day_entries:[{id:1,note:'hi'}]})
|
17
|
+
assert_equal 'hi', HCl::DayEntry.today(http).first.note
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_all_with_date
|
21
|
+
register_uri(:get, '/daily/013/2013', {projects:[], day_entries:[{id:1,note:'hi'}]})
|
22
|
+
assert_equal 'hi', HCl::DayEntry.daily(http,Date.civil(2013,1,13)).first.note
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_toggle
|
26
|
+
entry = HCl::DayEntry.new(id:123)
|
27
|
+
register_uri(:get, '/daily/timer/123', {note:'hi'})
|
28
|
+
entry.toggle http
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_cancel_success
|
32
|
+
entry = HCl::DayEntry.new(id:123)
|
33
|
+
register_uri(:delete, '/daily/delete/123')
|
34
|
+
assert entry.cancel http
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_cancel_failure
|
38
|
+
entry = HCl::DayEntry.new(id:123)
|
39
|
+
http.expects(:delete).raises(HCl::HarvestMiddleware::Failure)
|
40
|
+
assert !entry.cancel(http)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_to_s
|
44
|
+
entry = HCl::DayEntry.new \
|
45
|
+
hours: '1.2', client: 'Taco Town', project:'Pizza Taco', task:'Preparation'
|
46
|
+
assert_equal "Taco Town - Pizza Taco - Preparation (1:12)", entry.to_s
|
34
47
|
end
|
35
48
|
|
36
49
|
def test_append_note
|
37
50
|
entry = HCl::DayEntry.new(:id => '1', :notes => 'yourmom.', :hours => '1.0')
|
38
|
-
|
39
|
-
entry.append_note('hi world')
|
51
|
+
http.stubs(:post)
|
52
|
+
entry.append_note(http, 'hi world')
|
40
53
|
assert_equal "yourmom.\nhi world", entry.notes
|
41
54
|
end
|
42
55
|
|
43
56
|
def test_append_note_to_empty
|
44
57
|
entry = HCl::DayEntry.new(:id => '1', :notes => nil, :hours => '1.0')
|
45
|
-
|
46
|
-
entry.append_note('hi world')
|
58
|
+
http.stubs(:post)
|
59
|
+
entry.append_note(http, 'hi world')
|
47
60
|
assert_equal 'hi world', entry.notes
|
48
61
|
end
|
49
62
|
end
|
data/test/ext/capture_output.rb
CHANGED
data/test/net_test.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class NetTest < HCl::TestCase
|
4
|
+
|
5
|
+
def test_configure
|
6
|
+
assert_equal 'bob', http.login
|
7
|
+
assert_equal 'secret', http.password
|
8
|
+
assert_equal 'bobclock', http.subdomain
|
9
|
+
assert_equal true, http.ssl
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_http_deep_unescape
|
13
|
+
register_uri(:get, "/foo", {
|
14
|
+
status:'gotten & got!',
|
15
|
+
comparisons:['burrito > taco', 'rain < sun']
|
16
|
+
})
|
17
|
+
body = http.get 'foo'
|
18
|
+
assert_equal 'gotten & got!', body[:status]
|
19
|
+
assert_equal 'burrito > taco', body[:comparisons][0]
|
20
|
+
assert_equal 'rain < sun', body[:comparisons][1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_http_get
|
24
|
+
register_uri(:get, "/foo", {message:'gotten!'})
|
25
|
+
body = http.get 'foo'
|
26
|
+
assert_equal 'gotten!', body[:message]
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_http_post
|
30
|
+
register_uri(:post, "/foo", {message:'posted!'})
|
31
|
+
body = http.post 'foo', {pizza:'taco'}
|
32
|
+
assert_equal 'posted!', body[:message]
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_http_delete
|
36
|
+
register_uri(:delete, "/foo", {message:'wiped!'})
|
37
|
+
body = http.delete 'foo'
|
38
|
+
assert_equal 'wiped!', body[:message]
|
39
|
+
end
|
40
|
+
end
|
data/test/task_test.rb
CHANGED
@@ -1,30 +1,47 @@
|
|
1
1
|
|
2
|
-
class
|
2
|
+
class TaskTest < HCl::TestCase
|
3
3
|
def test_cache_file
|
4
4
|
assert_equal "#{HCl::App::HCL_DIR}/cache/tasks.yml", HCl::Task.cache_file
|
5
5
|
end
|
6
6
|
|
7
|
-
def
|
8
|
-
HCl::Task.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
<name>Security support</name>
|
19
|
-
<id type="integer">14</id>
|
20
|
-
<billable type="boolean">true</billable>
|
21
|
-
</task>
|
22
|
-
</tasks>
|
23
|
-
</project>
|
24
|
-
</projects>
|
25
|
-
</daily>
|
26
|
-
EOD
|
7
|
+
def test_cache_tasks_hash
|
8
|
+
HCl::Task.cache_tasks_hash({ projects: [{
|
9
|
+
name: "Click and Type",
|
10
|
+
id: 3,
|
11
|
+
client: "AFS",
|
12
|
+
tasks: [{
|
13
|
+
name: "Security support",
|
14
|
+
id: 14,
|
15
|
+
billable: true
|
16
|
+
}]
|
17
|
+
}]})
|
27
18
|
assert_equal 1, HCl::Task.all.size
|
28
19
|
assert_equal 'Security support', HCl::Task.all.first.name
|
29
20
|
end
|
21
|
+
|
22
|
+
def test_add
|
23
|
+
task = HCl::Task.new(id:1, project:HCl::Project.new({id:2}))
|
24
|
+
register_uri(:post, '/daily/add', {
|
25
|
+
note:'good stuff', hours:0.2, project_id:2, task_id:1, spent_at: Date.today})
|
26
|
+
entry = task.add(http, note:'good stuff', starting_time:0.2)
|
27
|
+
assert_equal 'good stuff', entry.note
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_start_running
|
31
|
+
task = HCl::Task.new(id:1, project:HCl::Project.new({id:2}))
|
32
|
+
register_uri(:post, '/daily/add', {
|
33
|
+
note:'good stuff', timer_started_at:DateTime.now,
|
34
|
+
hours:0.2, project_id:2, task_id:1, spent_at: Date.today})
|
35
|
+
entry = task.start(http, note:'good stuff', starting_time:0.2)
|
36
|
+
assert_equal 'good stuff', entry.note
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_start_then_toggle
|
40
|
+
task = HCl::Task.new(id:1, project:HCl::Project.new({id:2}))
|
41
|
+
register_uri(:post, '/daily/add', {id:123, note:'woot'})
|
42
|
+
register_uri(:get, '/daily/timer/123', {note:'good stuff', hours:0.2,
|
43
|
+
project_id:2, task_id:1, spent_at: Date.today})
|
44
|
+
entry = task.start(http, note:'good stuff', starting_time:0.2)
|
45
|
+
assert_equal 'good stuff', entry.note
|
46
|
+
end
|
30
47
|
end
|
data/test/test_helper.rb
CHANGED
@@ -9,11 +9,7 @@ begin
|
|
9
9
|
source_file.lines.count < 15
|
10
10
|
end
|
11
11
|
# source: https://travis-ci.org/zenhob/hcl
|
12
|
-
minimum_coverage
|
13
|
-
when "rbx" then 84
|
14
|
-
when "jruby" then 73
|
15
|
-
else 78
|
16
|
-
end
|
12
|
+
minimum_coverage 84
|
17
13
|
end
|
18
14
|
rescue LoadError => e
|
19
15
|
$stderr.puts 'No test coverage tools found, skipping coverage check.'
|
@@ -31,5 +27,25 @@ require 'fakeweb'
|
|
31
27
|
# require test extensions/helpers
|
32
28
|
Dir[File.dirname(__FILE__) + '/ext/*.rb'].each { |ext| require ext }
|
33
29
|
|
34
|
-
class HCl::TestCase < MiniTest::Unit::TestCase
|
30
|
+
class HCl::TestCase < MiniTest::Unit::TestCase
|
31
|
+
attr_reader :http
|
32
|
+
def setup
|
33
|
+
FakeWeb.allow_net_connect = false
|
34
|
+
@http = HCl::Net.new \
|
35
|
+
'login' => 'bob',
|
36
|
+
'password' => 'secret',
|
37
|
+
'subdomain' => 'bobclock',
|
38
|
+
'ssl' => true
|
39
|
+
end
|
40
|
+
|
41
|
+
def register_uri method, path, data={}
|
42
|
+
FakeWeb.register_uri(method, "https://bob:secret@bobclock.harvestapp.com#{path}",
|
43
|
+
body: Yajl::Encoder.encode(data))
|
44
|
+
end
|
45
|
+
|
46
|
+
def teardown
|
47
|
+
FakeWeb.clean_registry
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
35
51
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hcl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zack Hobson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: trollop
|
@@ -52,6 +52,62 @@ dependencies:
|
|
52
52
|
- - '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: faraday
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yajl-ruby
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: escape_utils
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
55
111
|
- !ruby/object:Gem::Dependency
|
56
112
|
name: rake
|
57
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,7 +222,10 @@ files:
|
|
166
222
|
- bin/hcl
|
167
223
|
- lib/hcl/app.rb
|
168
224
|
- lib/hcl/commands.rb
|
225
|
+
- lib/hcl/console.rb
|
169
226
|
- lib/hcl/day_entry.rb
|
227
|
+
- lib/hcl/harvest_middleware.rb
|
228
|
+
- lib/hcl/net.rb
|
170
229
|
- lib/hcl/project.rb
|
171
230
|
- lib/hcl/task.rb
|
172
231
|
- lib/hcl/timesheet_resource.rb
|
@@ -177,9 +236,9 @@ files:
|
|
177
236
|
- test/command_test.rb
|
178
237
|
- test/day_entry_test.rb
|
179
238
|
- test/ext/capture_output.rb
|
239
|
+
- test/net_test.rb
|
180
240
|
- test/task_test.rb
|
181
241
|
- test/test_helper.rb
|
182
|
-
- test/timesheet_resource_test.rb
|
183
242
|
- test/utility_test.rb
|
184
243
|
homepage: http://zackhobson.com/hcl/
|
185
244
|
licenses:
|
@@ -1,38 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
class TimesheetResourceTest < HCl::TestCase
|
4
|
-
|
5
|
-
def setup
|
6
|
-
FakeWeb.allow_net_connect = false
|
7
|
-
HCl::TimesheetResource.configure \
|
8
|
-
'login' => 'bob',
|
9
|
-
'password' => 'secret',
|
10
|
-
'subdomain' => 'bobclock',
|
11
|
-
'ssl' => true
|
12
|
-
end
|
13
|
-
|
14
|
-
def test_configure
|
15
|
-
assert_equal 'bob', HCl::TimesheetResource.login
|
16
|
-
assert_equal 'secret', HCl::TimesheetResource.password
|
17
|
-
assert_equal 'bobclock', HCl::TimesheetResource.subdomain
|
18
|
-
assert_equal true, HCl::TimesheetResource.ssl
|
19
|
-
end
|
20
|
-
|
21
|
-
def test_http_get
|
22
|
-
FakeWeb.register_uri(:get, "https://bob:secret@bobclock.harvestapp.com/foo", :body => 'gotten!')
|
23
|
-
body = HCl::TimesheetResource.get 'foo'
|
24
|
-
assert_equal 'gotten!', body
|
25
|
-
end
|
26
|
-
|
27
|
-
def test_http_post
|
28
|
-
FakeWeb.register_uri(:post, "https://bob:secret@bobclock.harvestapp.com/foo", :body => 'posted!')
|
29
|
-
body = HCl::TimesheetResource.post 'foo', {pizza:'taco'}
|
30
|
-
assert_equal 'posted!', body
|
31
|
-
end
|
32
|
-
|
33
|
-
def test_http_delete
|
34
|
-
FakeWeb.register_uri(:delete, "https://bob:secret@bobclock.harvestapp.com/foo", :body => 'wiped!')
|
35
|
-
body = HCl::TimesheetResource.delete 'foo'
|
36
|
-
assert_equal 'wiped!', body
|
37
|
-
end
|
38
|
-
end
|