the86-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +80 -0
- data/Rakefile +11 -0
- data/lib/the86-client.rb +69 -0
- data/lib/the86-client/active_model.rb +37 -0
- data/lib/the86-client/connection.rb +81 -0
- data/lib/the86-client/conversation.rb +14 -0
- data/lib/the86-client/errors.rb +20 -0
- data/lib/the86-client/oauth_bearer_authorization.rb +21 -0
- data/lib/the86-client/post.rb +26 -0
- data/lib/the86-client/resource.rb +149 -0
- data/lib/the86-client/resource_collection.rb +66 -0
- data/lib/the86-client/site.rb +18 -0
- data/lib/the86-client/user.rb +14 -0
- data/lib/the86-client/version.rb +5 -0
- data/spec/conversations_spec.rb +75 -0
- data/spec/oauth_bearer_authorization_spec.rb +19 -0
- data/spec/post_spec.rb +15 -0
- data/spec/posts_spec.rb +81 -0
- data/spec/resource_spec.rb +32 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/webmock.rb +41 -0
- data/spec/users_spec.rb +78 -0
- data/the86-client.gemspec +47 -0
- metadata +256 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module The86::Client
|
2
|
+
class ResourceCollection
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Connection is a The86::Client::Connection instance.
|
7
|
+
# Path is the API-relative path, e.g. "users".
|
8
|
+
# Klass is class of each record in the collection, e.g. User
|
9
|
+
# Attributes is a Hash of attributes common to all items in collection,
|
10
|
+
# and not fetched in HTTP response, e.g. parent items.
|
11
|
+
# e.g. for conversations: { site: Site.new(slug: "...") }
|
12
|
+
# Records is an array of hashes, for pre-populating the collection.
|
13
|
+
# e.g. when an API response contains collections of child resources.
|
14
|
+
def initialize(connection, path, klass, parent, records = nil)
|
15
|
+
@connection = connection
|
16
|
+
@path = path
|
17
|
+
@klass = klass
|
18
|
+
@parent = parent
|
19
|
+
@records = records
|
20
|
+
end
|
21
|
+
|
22
|
+
def build(attributes)
|
23
|
+
@klass.new(attributes.merge(parent: @parent))
|
24
|
+
end
|
25
|
+
|
26
|
+
def create(attributes)
|
27
|
+
build(attributes).tap(&:save)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Find and load a resource.
|
31
|
+
#
|
32
|
+
# If Resource#url_id is overridden, specify the attribute name.
|
33
|
+
# TODO: the resource should know its URL attribute name.
|
34
|
+
#
|
35
|
+
# Note that this currently triggers an HTTP GET, then a POST:
|
36
|
+
# conversation.find(10).posts.create(attributes)
|
37
|
+
# As an alternative, this only triggers the HTTP POST:
|
38
|
+
# conversation.build(id: 10).posts.create(attributes)
|
39
|
+
def find(id, attribute = :id)
|
40
|
+
build(id: id).load
|
41
|
+
end
|
42
|
+
|
43
|
+
def each
|
44
|
+
records.each do |attributes|
|
45
|
+
yield build(attributes)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Cache array representation.
|
50
|
+
# Save building Resources for each record multiple times.
|
51
|
+
def to_a
|
52
|
+
@_to_a = super
|
53
|
+
end
|
54
|
+
|
55
|
+
def [](index)
|
56
|
+
to_a[index]
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def records
|
62
|
+
@records ||= @connection.get(path: @path, status: 200)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module The86::Client
|
2
|
+
class Site < Resource
|
3
|
+
|
4
|
+
attribute :id, Integer
|
5
|
+
attribute :name, String
|
6
|
+
attribute :slug, String
|
7
|
+
attribute :created_at, DateTime
|
8
|
+
attribute :updated_at, DateTime
|
9
|
+
|
10
|
+
path "sites"
|
11
|
+
has_many :conversations, ->{ Conversation }
|
12
|
+
|
13
|
+
def url_id
|
14
|
+
slug
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
|
5
|
+
describe "Conversations" do
|
6
|
+
|
7
|
+
describe "listing conversations" do
|
8
|
+
it "returns empty array for site without conversations" do
|
9
|
+
expect_get_conversations(response_body: [])
|
10
|
+
site.conversations.to_a.size.must_equal 0
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns collection of conversations" do
|
14
|
+
expect_get_conversations(response_body: [{id: 10}, {id: 12}])
|
15
|
+
conversations = site.conversations
|
16
|
+
conversations.to_a.size.must_equal 2
|
17
|
+
c = conversations.first
|
18
|
+
c.must_be_instance_of Conversation
|
19
|
+
c.id.must_equal 10
|
20
|
+
c.site.must_equal site
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "creating conversations" do
|
25
|
+
it "posts and returns a conversation with the first post content" do
|
26
|
+
expect_request(
|
27
|
+
url: "https://example.org/api/v1/sites/test/conversations",
|
28
|
+
method: :post,
|
29
|
+
status: 201,
|
30
|
+
request_body: {content: "A new conversation."},
|
31
|
+
response_body: {id: 2, posts: [{id: 5, content: "A new conversation."}]},
|
32
|
+
request_headers: {"Authorization" => "Bearer secrettoken"},
|
33
|
+
)
|
34
|
+
|
35
|
+
c = site.conversations.create(
|
36
|
+
content: "A new conversation.",
|
37
|
+
oauth_token: "secrettoken",
|
38
|
+
)
|
39
|
+
|
40
|
+
c.id.must_equal 2
|
41
|
+
posts = c.posts
|
42
|
+
posts.to_a.length.must_equal 1
|
43
|
+
posts[0].content.must_equal "A new conversation."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "finding a conversation" do
|
48
|
+
it "gets the conversation, loads data into the resource" do
|
49
|
+
expect_request(
|
50
|
+
url: "https://user:pass@example.org/api/v1/sites/test/conversations/4",
|
51
|
+
method: :get,
|
52
|
+
status: 200,
|
53
|
+
response_body: {id: 4, posts: [{id: 8, content: "A post."}]},
|
54
|
+
)
|
55
|
+
c = site.conversations.find(4)
|
56
|
+
c.id.must_equal 4
|
57
|
+
c.posts.first.content.must_equal "A post."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def site
|
62
|
+
The86::Client.site("test")
|
63
|
+
end
|
64
|
+
|
65
|
+
def expect_get_conversations(options)
|
66
|
+
expect_request({
|
67
|
+
url: "https://user:pass@example.org/api/v1/sites/test/conversations",
|
68
|
+
method: :get,
|
69
|
+
status: 200,
|
70
|
+
}.merge(options))
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
describe OauthBearerAuthorization do
|
5
|
+
|
6
|
+
it "sets Authorization header, calls delegate" do
|
7
|
+
env = {request_headers: {}}
|
8
|
+
|
9
|
+
app = MiniTest::Mock.new
|
10
|
+
app.expect(:call, nil, [env])
|
11
|
+
|
12
|
+
OauthBearerAuthorization.new(app, "secret").call(env)
|
13
|
+
|
14
|
+
env[:request_headers]["Authorization"].must_equal "Bearer secret"
|
15
|
+
app.verify
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/spec/post_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
require "the86-client/post"
|
3
|
+
|
4
|
+
module The86::Client
|
5
|
+
describe Post do
|
6
|
+
describe "#reply?" do
|
7
|
+
it "is true when in_reply_to_id is present" do
|
8
|
+
Post.new(in_reply_to_id: 32).reply?.must_equal true
|
9
|
+
end
|
10
|
+
it "is false when in_reply_to_id is not present" do
|
11
|
+
Post.new(in_reply_to_id: nil).reply?.must_equal false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/posts_spec.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
|
5
|
+
describe Post do
|
6
|
+
|
7
|
+
describe "replying to a post" do
|
8
|
+
it "sends in_reply_to_id" do
|
9
|
+
expect_request(
|
10
|
+
url: "https://example.org/api/v1/sites/test/conversations/32/posts",
|
11
|
+
method: :post,
|
12
|
+
status: 201,
|
13
|
+
request_body: {content: "Hi!", in_reply_to_id: 64},
|
14
|
+
response_body: {
|
15
|
+
id: 96, content: "Hi!", in_reply_to_id: 64, in_reply_to: {
|
16
|
+
id: 64,
|
17
|
+
content: "Hello!",
|
18
|
+
user: {
|
19
|
+
id: 128,
|
20
|
+
name: "Johnny Original"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
},
|
24
|
+
request_headers: {"Authorization" => "Bearer SecretTokenHere"},
|
25
|
+
)
|
26
|
+
post = original_post.reply(
|
27
|
+
content: "Hi!",
|
28
|
+
oauth_token: "SecretTokenHere",
|
29
|
+
)
|
30
|
+
post.conversation.id.must_equal conversation.id
|
31
|
+
post.in_reply_to_id.must_equal original_post.id
|
32
|
+
post.in_reply_to.user.name.must_equal "Johnny Original"
|
33
|
+
post.content.must_equal "Hi!"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "following up to a conversation" do
|
38
|
+
it "creates new Post in the Conversation" do
|
39
|
+
expect_request(
|
40
|
+
url: "https://example.org/api/v1/sites/test/conversations/32/posts",
|
41
|
+
method: :post,
|
42
|
+
status: 201,
|
43
|
+
request_body: {content: "+1"},
|
44
|
+
response_body: {id: 96, content: "+1"},
|
45
|
+
request_headers: {"Authorization" => "Bearer SecretTokenHere"},
|
46
|
+
)
|
47
|
+
post = conversation.posts.create(
|
48
|
+
content: "+1",
|
49
|
+
oauth_token: "SecretTokenHere",
|
50
|
+
)
|
51
|
+
post.conversation.id.must_equal conversation.id
|
52
|
+
post.in_reply_to_id.must_equal nil
|
53
|
+
post.content.must_equal "+1"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#user" do
|
58
|
+
let(:post) { Post.new(id: 1, user: {id: 2, name: "John Citizen"}) }
|
59
|
+
it "returns instance of The86::Client::User" do
|
60
|
+
post.user.must_be_instance_of(The86::Client::User)
|
61
|
+
end
|
62
|
+
it "contains the user details" do
|
63
|
+
post.user.name.must_equal "John Citizen"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def original_post
|
68
|
+
Post.new(id: 64, conversation: conversation, content: "Hello!")
|
69
|
+
end
|
70
|
+
|
71
|
+
def conversation
|
72
|
+
Conversation.new(id: 32, site: site)
|
73
|
+
end
|
74
|
+
|
75
|
+
def site
|
76
|
+
The86::Client.site("test")
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
describe Resource do
|
5
|
+
|
6
|
+
describe "equality" do
|
7
|
+
resource = Class.new(Resource) do
|
8
|
+
attribute :id, Integer
|
9
|
+
attribute :code, String
|
10
|
+
end
|
11
|
+
|
12
|
+
parent_resource = Class.new(Resource) do
|
13
|
+
attribute :id, Integer
|
14
|
+
end
|
15
|
+
|
16
|
+
it "is equal by attributes" do
|
17
|
+
resource.new(id: 1, code: "A").
|
18
|
+
must_equal resource.new(id: 1, code: "A")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "is inequal by attributes" do
|
22
|
+
resource.new(id: 2, code: "A").
|
23
|
+
wont_equal resource.new(id: 1, code: "A")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "is inequal by parent" do
|
27
|
+
resource.new(code: "A", parent: parent_resource.new(id: 1)).
|
28
|
+
wont_equal resource.new(code: "A", parent: parent_resource.new(id: 2))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "json"
|
2
|
+
require "webmock/minitest"
|
3
|
+
|
4
|
+
WebMock.disable_net_connect!
|
5
|
+
|
6
|
+
##
|
7
|
+
# WebMock request expectations.
|
8
|
+
module RequestExpectations
|
9
|
+
|
10
|
+
def self.included(klass)
|
11
|
+
klass.before { @expected_requests = [] }
|
12
|
+
klass.after { @expected_requests.each { |sr| assert_requested(sr) } }
|
13
|
+
end
|
14
|
+
|
15
|
+
def stub_and_assert_request(*parameters)
|
16
|
+
stub_request(*parameters).tap do |request|
|
17
|
+
@expected_requests << request
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def expect_request(options)
|
22
|
+
url = options.fetch(:url)
|
23
|
+
method = options.fetch(:method)
|
24
|
+
request_body = options[:request_body]
|
25
|
+
response_body = options[:response_body]
|
26
|
+
request_headers = options[:request_headers]
|
27
|
+
|
28
|
+
request = {}
|
29
|
+
request[:body] = request_body if request_body
|
30
|
+
request[:headers] = request_headers if request_headers
|
31
|
+
|
32
|
+
response = {status: options[:status] || 200}
|
33
|
+
response[:body] = JSON.generate(response_body) if response_body
|
34
|
+
|
35
|
+
stub_and_assert_request(method, url).
|
36
|
+
with(request).
|
37
|
+
to_return(response)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
MiniTest::Spec.send(:include, RequestExpectations)
|
data/spec/users_spec.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
|
5
|
+
describe "Users" do
|
6
|
+
|
7
|
+
describe "creating a user" do
|
8
|
+
it "is successful with 200 OK" do
|
9
|
+
expect_create_user(response_body: {id: 1, name: "John Appleseed"})
|
10
|
+
user = The86::Client.users.create(name: "John Appleseed")
|
11
|
+
user.id.must_be_instance_of Fixnum
|
12
|
+
user.name.must_equal "John Appleseed"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "raises error for 200 OK" do
|
16
|
+
expect_create_user(status: 200, response_body: {id: 1, name: "John Appleseed"})
|
17
|
+
->{ The86::Client.users.create(name: "John Appleseed") }.must_raise Error
|
18
|
+
end
|
19
|
+
|
20
|
+
it "raises ValidationFailed for 422 response" do
|
21
|
+
expect_create_user(status: 422, request_body: {name: ""})
|
22
|
+
->{ The86::Client.users.create(name: "") }.must_raise ValidationFailed
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises Unauthorized for 401 response" do
|
26
|
+
expect_create_user(status: 401, request_body: {name: "Test"})
|
27
|
+
->{ The86::Client.users.create(name: "Test") }.must_raise Unauthorized
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "updating a user (may not be supported by server)" do
|
32
|
+
it "PATCHes users/:id" do
|
33
|
+
time_before = DateTime.now - 86400
|
34
|
+
time_after = DateTime.now
|
35
|
+
expect_request(
|
36
|
+
url: "https://user:pass@example.org/api/v1/users/5",
|
37
|
+
method: :patch,
|
38
|
+
status: 200,
|
39
|
+
request_body: {name: "New Name"},
|
40
|
+
response_body: {id: 5, name: "New Name", updated_at: time_after}
|
41
|
+
)
|
42
|
+
user = User.new(
|
43
|
+
id: 5,
|
44
|
+
name: "Old Name",
|
45
|
+
updated_at: time_before
|
46
|
+
)
|
47
|
+
user.name = "New Name"
|
48
|
+
user.updated_at.to_s.must_equal time_before.to_s
|
49
|
+
user.save
|
50
|
+
user.updated_at.to_s.must_equal time_after.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "listing users (may not be supported by server)" do
|
55
|
+
it "returns collection of users" do
|
56
|
+
expect_request(
|
57
|
+
url: "https://user:pass@example.org/api/v1/users",
|
58
|
+
method: :get,
|
59
|
+
status: 200,
|
60
|
+
response_body: [{id: 4, name: "Paul"}, {id: 8, name: "James"}]
|
61
|
+
)
|
62
|
+
users = The86::Client.users
|
63
|
+
users.to_a.length.must_equal 2
|
64
|
+
users.first.name.must_equal "Paul"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def expect_create_user(options = {})
|
69
|
+
expect_request({
|
70
|
+
url: "https://user:pass@example.org/api/v1/users",
|
71
|
+
method: :post,
|
72
|
+
status: 201,
|
73
|
+
}.merge(options))
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|