hcl 0.4.9 → 0.4.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/zenhob/hcl.png?branch=master)](https://travis-ci.org/zenhob/hcl)
|
9
9
|
[![Gem Version](https://badge.fury.io/rb/hcl.png)](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
|