snitcher 0.3.2 → 0.4.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ module Snitcher::API
2
+ # Error is the base class for all API specific errors. For a full list of
3
+ # errors and how they can happen please refer to the API documentation.
4
+ #
5
+ # https://deadmanssnitch.com/docs/api/v1#error-reference
6
+ class Error < StandardError
7
+ attr_reader :type
8
+
9
+ def self.new(api_error)
10
+ type = api_error.delete("type")
11
+ message = api_error.delete("error")
12
+
13
+ klass =
14
+ case type.to_s
15
+ # sign_in_incorrect is only returned when using username + password.
16
+ when "sign_in_incorrect"; AuthenticationError
17
+ # api_key_invalid is only returned when using the API key.
18
+ when "api_key_invalid"; AuthenticationError
19
+ when "plan_limit_reached"; PlanLimitReachedError
20
+ when "account_on_hold"; AccountOnHoldError
21
+ when "resource_not_found"; ResourceNotFoundError
22
+ when "resource_invalid"; ResourceInvalidError
23
+ else Error
24
+ end
25
+
26
+ error = klass.allocate
27
+ error.send(:initialize, type, message, api_error)
28
+ error
29
+ end
30
+
31
+ def initialize(type, message = nil, metadata = nil)
32
+ super(message)
33
+
34
+ @type = type
35
+ @metadata = metadata || {}
36
+ end
37
+ end
38
+
39
+ # AuthenticationError is raised from API calls when the given credentials
40
+ # are invalid.
41
+ class AuthenticationError < Error; end
42
+
43
+ # PlanLimitReachedError is raised when a request fails due to that feature
44
+ # being limited by your current plan. Most likely this is due to having too
45
+ # many snitches.
46
+ class PlanLimitReachedError < Error; end
47
+
48
+ # AccountOnHoldError is raised when an account becomes delinquent due to
49
+ # payment being rejected. This can be thrown from an API request and this can
50
+ # be fixed by updating the credit card on file at:
51
+ # https://deadmanssnitch.com/account/billing
52
+ class AccountOnHoldError < Error; end
53
+
54
+ # ResourceNotFoundError is raised when requesting a Snitch or other resource
55
+ # that does not exist or you do not have permission to.
56
+ class ResourceNotFoundError < Error; end
57
+
58
+ # ResourceInvalidError is raised when updating a resource and there are errors
59
+ # with the update.
60
+ class ResourceInvalidError < Error
61
+ def errors
62
+ @metadata.fetch("validations", []).each_with_object({}) do |tuple, memo|
63
+ memo[tuple["attribute"]] = tuple["message"]
64
+ end
65
+ end
66
+ end
67
+
68
+ # InternalServerError is raised when something bad has happened on our end.
69
+ # Hopefully it's nothing you did and we're already on the case getting it
70
+ # fixed. If you're able to trigger this regularly please contact us as we
71
+ # could use your help reproducing it.
72
+ class InternalServerError < Error; end
73
+ end
@@ -0,0 +1,44 @@
1
+ class Snitcher::API::Snitch
2
+ attr_accessor :token, :name, :tags, :status, :checked_in_at,
3
+ :interval, :check_in_url, :created_at, :notes
4
+
5
+ # Public: Return a Snitcher::API::Snitch object based on a hash payload.
6
+ #
7
+ # Example
8
+ #
9
+ # payload = {
10
+ # "token" => "c2354d53d3",
11
+ # "href" => "/v1/snitches/c2354d53d3",
12
+ # "name" => "Daily Backups",
13
+ # "tags" => [
14
+ # "production",
15
+ # "critical"
16
+ # ],
17
+ # "status" => "pending",
18
+ # "checked_in_at" => "",
19
+ # "type": {
20
+ # "interval" => "daily"
21
+ # },
22
+ # "check_in_url" => "https://nosnch.in/c2354d53d3",
23
+ # "created_at" => "2015-08-15T12:15:00.234Z",
24
+ # "notes" => "Important user data.",
25
+ # }
26
+ #
27
+ # Snitcher::API::Snitch.new(payload)
28
+ # => #<Snitcher::API::Snitch:0x007fdcf50ad2d0 @token="c2354d53d3",
29
+ # @name="Daily Backups", @tags=["production", "critical"],
30
+ # @status="pending", @checked_in_at=nil, @interval="daily",
31
+ # @check_in_url="https://nosnch.in/c2354d53d3",
32
+ # @created_at="2015-08-15T12:15:00.234Z", @notes="Important user data.">
33
+ def initialize(payload)
34
+ @token = payload["token"]
35
+ @name = payload["name"]
36
+ @tags = payload["tags"]
37
+ @status = payload["status"]
38
+ @checked_in_at = payload["checked_in_at"]
39
+ @interval = payload["type"]["interval"]
40
+ @check_in_url = payload["check_in_url"]
41
+ @created_at = payload["created_at"]
42
+ @notes = payload["notes"]
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module Snitcher
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0.pre1"
3
3
  end
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(/^bin/) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(/^spec/)
19
19
 
20
+ spec.required_ruby_version = ">= 1.9.3"
21
+
20
22
  spec.add_development_dependency "bundler", "~> 1.5"
21
23
  spec.add_development_dependency "rake", "~> 10.1"
22
24
  end
@@ -0,0 +1,454 @@
1
+ require "spec_helper"
2
+ require "snitcher/api/client"
3
+ require "base64"
4
+ require "securerandom"
5
+
6
+ describe Snitcher::API::Client do
7
+ subject(:client) do
8
+ Snitcher::API::Client.new("key", endpoint: "http://api.dms.dev")
9
+ end
10
+
11
+ let(:stub_url) { /api\.dms\.dev/ }
12
+ let(:snitch_url) { "http://key:@api.dms.dev/v1/snitches" }
13
+
14
+ describe "#snitches" do
15
+ let(:url) { snitch_url }
16
+ let(:body) { '[
17
+ {
18
+ "token": "agr0683qp4",
19
+ "href": "/v1/snitches/agr0683qp4",
20
+ "name": "Cool Test Snitch",
21
+ "tags": [
22
+ "testing",
23
+ "api"
24
+ ],
25
+ "status": "pending",
26
+ "checked_in_at": "",
27
+ "type": {
28
+ "interval": "hourly"
29
+ }
30
+ },
31
+ {
32
+ "token": "xyz8574uy2",
33
+ "href": "/v1/snitches/xyz8574uy2",
34
+ "name": "Even Cooler Test Snitch",
35
+ "tags": [
36
+ "testing"
37
+ ],
38
+ "status": "pending",
39
+ "checked_in_at": "",
40
+ "type": {
41
+ "interval": "hourly"
42
+ }
43
+ }
44
+ ]'
45
+ }
46
+
47
+ before do
48
+ stub_request(:get, stub_url).to_return(:body => body, :status => 200)
49
+ end
50
+
51
+ it "pings API with the api_key" do
52
+ client.snitches
53
+
54
+ expect(a_request(:get, url)).to have_been_made.once
55
+ end
56
+
57
+ it "returns the array of snitches" do
58
+ expect(client.snitches).to be_a(Array)
59
+ expect(client.snitches.first).to be_a(Snitcher::API::Snitch)
60
+ end
61
+ end
62
+
63
+ describe "#snitch" do
64
+ let(:token) { "c2354d53d2" }
65
+ let(:url) { "#{snitch_url}/#{token}" }
66
+ let(:body) { '{
67
+ "token": "c2354d53d2",
68
+ "href": "/v1/snitches/c2354d53d2",
69
+ "name": "Cool Test Snitch",
70
+ "tags": [
71
+ "testing",
72
+ "api"
73
+ ],
74
+ "status": "pending",
75
+ "checked_in_at": "",
76
+ "type": {
77
+ "interval": "hourly"
78
+ },
79
+ "check_in_url": "https://nosnch.in/c2354d53d2",
80
+ "created_at": "2015-08-15T12:15:00.234Z",
81
+ "notes": "Save everything that is cool."
82
+ }'
83
+ }
84
+
85
+ before do
86
+ stub_request(:get, stub_url).to_return(:body => body, :status => 200)
87
+ end
88
+
89
+ it "pings API with the api_key" do
90
+ client.snitch(token)
91
+
92
+ expect(a_request(:get, url)).to have_been_made.once
93
+ end
94
+
95
+ it "returns the snitch" do
96
+ expect(client.snitch(token)).to be_a(Snitcher::API::Snitch)
97
+ end
98
+ end
99
+
100
+ describe "#tagged_snitches" do
101
+ let(:tags) { ["sneetch", "belly"] }
102
+ let(:url) { "#{snitch_url}?tags=sneetch,belly" }
103
+ let(:body) { '[
104
+ {
105
+ "token": "c2354d53d2",
106
+ "href": "/v1/snitches/c2354d53d2",
107
+ "name": "Best Kind of Sneetch on the Beach",
108
+ "tags": [
109
+ "sneetch",
110
+ "belly",
111
+ "star-belly"
112
+ ],
113
+ "status": "pending",
114
+ "checked_in_at": "",
115
+ "type": {
116
+ "interval": "hourly"
117
+ }
118
+ },
119
+ {
120
+ "token": "c2354d53d3",
121
+ "href": "/v1/snitches/c2354d53d3",
122
+ "name": "Have None Upon Thars",
123
+ "tags": [
124
+ "sneetch",
125
+ "belly",
126
+ "plain-belly"
127
+ ],
128
+ "status": "pending",
129
+ "checked_in_at": "",
130
+ "type": {
131
+ "interval": "hourly"
132
+ }
133
+ }
134
+ ]'
135
+ }
136
+
137
+ before do
138
+ stub_request(:get, stub_url).to_return(:body => body, :status => 200)
139
+ end
140
+
141
+ it "pings API with the api_key" do
142
+ client.tagged_snitches(tags)
143
+
144
+ expect(a_request(:get, url)).to have_been_made.once
145
+ end
146
+
147
+ it "returns the snitches" do
148
+ expect(client.tagged_snitches(tags)).to be_a(Array)
149
+ expect(client.tagged_snitches(tags).first).to be_a(Snitcher::API::Snitch)
150
+ end
151
+
152
+ it "supports spaces in tags" do
153
+ request = stub_request(:get, "#{snitch_url}?tags=phoenix%20foundary,murggle").
154
+ to_return(body: body, status: 200)
155
+
156
+ client.tagged_snitches("phoenix foundary", "murggle")
157
+
158
+ expect(request).to have_been_made.once
159
+ end
160
+
161
+ it "allows an array to be passed for tags" do
162
+ request = stub_request(:get, "#{snitch_url}?tags=murggle,gurgggle").
163
+ to_return(body: body, status: 200)
164
+
165
+ client.tagged_snitches(["murggle", "gurgggle"])
166
+
167
+ expect(request).to have_been_made.once
168
+ end
169
+ end
170
+
171
+ describe "#create_snitch" do
172
+ let(:data) {
173
+ {
174
+ "name" => "Daily Backups",
175
+ "interval" => "daily",
176
+ "notes" => "Customer and supplier tables",
177
+ "tags" => ["backups", "maintenance"]
178
+ }
179
+ }
180
+ let(:url) { snitch_url }
181
+ let(:body) { '{
182
+ "token": "c2354d53d2",
183
+ "href": "/v1/snitches/c2354d53d2",
184
+ "name": "Daily Backups",
185
+ "tags": [
186
+ "backups",
187
+ "maintenance"
188
+ ],
189
+ "status": "pending",
190
+ "checked_in_at": "",
191
+ "type": {
192
+ "interval": "daily"
193
+ },
194
+ "check_in_url": "https://nosnch.in/c2354d53d2",
195
+ "created_at": "2015-08-27T18:30:23.737Z",
196
+ "notes": "Customer and supplier tables"
197
+ }'
198
+ }
199
+
200
+ before do
201
+ stub_request(:post, stub_url).to_return(:body => body, :status => 200)
202
+ end
203
+
204
+ it "pings API with the api_key" do
205
+ client.create_snitch(data)
206
+
207
+ expect(a_request(:post, url)).to have_been_made.once
208
+ end
209
+
210
+ it "returns the new snitch" do
211
+ expect(client.create_snitch(data)).to be_a(Snitcher::API::Snitch)
212
+ end
213
+
214
+ describe "validation errors" do
215
+ let(:data) do
216
+ {
217
+ "name" => "",
218
+ "interval" => "",
219
+ }
220
+ end
221
+
222
+ let(:body) do
223
+ '{
224
+ "type": "resource_invalid",
225
+ "error": "resource invalid",
226
+ "validations": [
227
+ { "attribute": "name", "message": "Can\'t be blank."},
228
+ { "attribute": "type.interval", "message": "Can\'t be blank."}
229
+ ]
230
+ }'
231
+ end
232
+
233
+ it "raises ResourceInvalidError if invalid" do
234
+ stub_request(:post, stub_url).to_return(:body => body, :status => 422)
235
+
236
+ expect {
237
+ client.create_snitch(data)
238
+ }.to raise_error(Snitcher::API::ResourceInvalidError) { |error|
239
+ expect(error.errors).to eq({
240
+ "name" => "Can't be blank.",
241
+ "type.interval" => "Can't be blank.",
242
+ })
243
+ }
244
+ end
245
+ end
246
+ end
247
+
248
+ describe "#edit_snitch" do
249
+ let(:token) { "c2354d53d2" }
250
+ let(:data) {
251
+ {
252
+ "interval" => "hourly",
253
+ "notes" => "We need this more often",
254
+ }
255
+ }
256
+ let(:url) { "#{snitch_url}/#{token}" }
257
+ let(:body) { '{
258
+ "token": "c2354d53d2",
259
+ "href": "/v1/snitches/c2354d53d2",
260
+ "name": "The Backups",
261
+ "tags": [
262
+ "backups",
263
+ "maintenance"
264
+ ],
265
+ "status": "pending",
266
+ "checked_in_at": "",
267
+ "type": {
268
+ "interval": "hourly"
269
+ },
270
+ "notes": "We need this more often"
271
+ }'
272
+ }
273
+
274
+ before do
275
+ stub_request(:patch, stub_url).to_return(:body => body, :status => 200)
276
+ end
277
+
278
+ it "pings API with the api_key" do
279
+ client.edit_snitch(token, data)
280
+
281
+ expect(a_request(:patch, url)).to have_been_made.once
282
+ end
283
+
284
+ it "returns the modified snitch" do
285
+ expect(client.edit_snitch(token, data)).to be_a(Snitcher::API::Snitch)
286
+ end
287
+ end
288
+
289
+ describe "#add_tags" do
290
+ let(:token) { "c2354d53d2" }
291
+ let(:tags) { ["red", "green"] }
292
+ let(:url) { "#{snitch_url}/#{token}/tags" }
293
+ let(:body) { '[
294
+ "red",
295
+ "green"
296
+ ]'
297
+ }
298
+
299
+ before do
300
+ stub_request(:post, stub_url).to_return(:body => body, :status => 200)
301
+ end
302
+
303
+ it "pings API with the api_key" do
304
+ client.add_tags(token, tags)
305
+
306
+ expect(a_request(:post, url)).to have_been_made.once
307
+ end
308
+
309
+ context "when successful" do
310
+ it "returns an array of the snitch's tags" do
311
+ expect(client.add_tags(token, tags)).to eq(JSON.parse(body))
312
+ end
313
+ end
314
+ end
315
+
316
+ describe "#remove_tag" do
317
+ let(:token) { "c2354d53d2" }
318
+ let(:tag) { "critical" }
319
+ let(:url) { "#{snitch_url}/#{token}/tags/#{tag}" }
320
+ let(:body) { '[
321
+ "critical"
322
+ ]'
323
+ }
324
+
325
+ before do
326
+ stub_request(:delete, stub_url).to_return(:body => body, :status => 200)
327
+ end
328
+
329
+ it "pings API with the api_key" do
330
+ client.remove_tag(token, tag)
331
+
332
+ expect(a_request(:delete, url)).to have_been_made.once
333
+ end
334
+
335
+ context "when successful" do
336
+ it "returns an array of the snitch's remaining tags" do
337
+ expect(client.remove_tag(token, tag)).to eq(JSON.parse(body))
338
+ end
339
+ end
340
+ end
341
+
342
+ describe "#replace_tags" do
343
+ let(:token) { "c2354d53d2" }
344
+ let(:tags) { ["red", "green"] }
345
+ let(:url) { "#{snitch_url}/#{token}" }
346
+ let(:body) { '{
347
+ "token": "c2354d53d2",
348
+ "href": "/v1/snitches/c2354d53d2",
349
+ "name": "Daily Backups",
350
+ "tags": [
351
+ "red",
352
+ "green"
353
+ ],
354
+ "status": "pending",
355
+ "checked_in_at": "",
356
+ "type": {
357
+ "interval": "daily"
358
+ },
359
+ "notes": "Sales data."
360
+ }'
361
+ }
362
+
363
+ before do
364
+ stub_request(:patch, stub_url).to_return(:body => body, :status => 200)
365
+ end
366
+
367
+ it "pings API with the api_key" do
368
+ client.replace_tags(token, tags)
369
+
370
+ expect(a_request(:patch, url)).to have_been_made.once
371
+ end
372
+
373
+ it "returns the updated snitch" do
374
+ expect(client.replace_tags(token, tags)).to be_a(Snitcher::API::Snitch)
375
+ end
376
+ end
377
+
378
+ describe "#clear_tags" do
379
+ let(:token) { "c2354d53d2" }
380
+ let(:url) { "#{snitch_url}/#{token}" }
381
+ let(:body) { '{
382
+ "token": "c2354d53d2",
383
+ "href": "/v1/snitches/c2354d53d2",
384
+ "name": "Daily Backups",
385
+ "tags": [
386
+ ],
387
+ "status": "pending",
388
+ "checked_in_at": "",
389
+ "type": {
390
+ "interval": "daily"
391
+ },
392
+ "notes": "Sales data."
393
+ }'
394
+ }
395
+
396
+ before do
397
+ stub_request(:patch, stub_url).to_return(:body => body, :status => 200)
398
+ end
399
+
400
+ it "pings API with the api_key" do
401
+ client.clear_tags(token)
402
+
403
+ expect(a_request(:patch, url)).to have_been_made.once
404
+ end
405
+
406
+ it "returns the updated snitch" do
407
+ expect(client.clear_tags(token)).to be_a(Snitcher::API::Snitch)
408
+ end
409
+ end
410
+
411
+ describe "#pause_snitch" do
412
+ let(:token) { "c2354d53d2" }
413
+ let(:url) { "#{snitch_url}/#{token}/pause" }
414
+ let(:body) { '{}' }
415
+
416
+ before do
417
+ stub_request(:post, stub_url).to_return(:body => body, :status => 200)
418
+ end
419
+
420
+ it "pings API with the api_key" do
421
+ client.pause_snitch(token)
422
+
423
+ expect(a_request(:post, url)).to have_been_made.once
424
+ end
425
+
426
+ context "when successful" do
427
+ it "returns an empty response" do
428
+ expect(client.pause_snitch(token)).to eq(JSON.parse(body))
429
+ end
430
+ end
431
+ end
432
+
433
+ describe "#delete_snitch" do
434
+ let(:token) { "c2354d53d2" }
435
+ let(:url) { "#{snitch_url}/#{token}" }
436
+ let(:body) { '{}' }
437
+
438
+ before do
439
+ stub_request(:delete, stub_url).to_return(:body => body, :status => 200)
440
+ end
441
+
442
+ it "pings API with the api_key" do
443
+ client.delete_snitch(token)
444
+
445
+ expect(a_request(:delete, url)).to have_been_made.once
446
+ end
447
+
448
+ context "when successful" do
449
+ it "returns an empty response" do
450
+ expect(client.delete_snitch(token)).to eq(JSON.parse(body))
451
+ end
452
+ end
453
+ end
454
+ end