snitcher 0.3.2 → 0.4.0.pre1

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.
@@ -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