ruby-trello 0.2.1 → 0.3.0
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.
- data/README.md +6 -1
- data/lib/trello.rb +23 -3
- data/lib/trello/action.rb +1 -0
- data/lib/trello/authorization.rb +77 -0
- data/lib/trello/basic_data.rb +1 -2
- data/lib/trello/board.rb +33 -16
- data/lib/trello/card.rb +24 -3
- data/lib/trello/checklist.rb +1 -0
- data/lib/trello/client.rb +33 -48
- data/lib/trello/item.rb +1 -0
- data/lib/trello/item_state.rb +1 -0
- data/lib/trello/list.rb +19 -0
- data/lib/trello/member.rb +1 -0
- data/lib/trello/net.rb +35 -0
- data/lib/trello/notification.rb +1 -0
- data/lib/trello/organization.rb +1 -0
- data/lib/trello/string.rb +1 -1
- data/spec/action_spec.rb +39 -10
- data/spec/basic_auth_policy_spec.rb +56 -0
- data/spec/board_spec.rb +156 -13
- data/spec/card_spec.rb +58 -23
- data/spec/client_spec.rb +123 -22
- data/spec/integration/how_to_authorize_spec.rb +53 -0
- data/spec/integration/how_to_use_boards_spec.rb +48 -0
- data/spec/integration/integration_test.rb +40 -0
- data/spec/list_spec.rb +15 -9
- data/spec/member_spec.rb +14 -11
- data/spec/oauth_policy_spec.rb +83 -0
- data/spec/spec_helper.rb +27 -11
- data/spec/string_spec.rb +50 -0
- metadata +23 -9
data/spec/client_spec.rb
CHANGED
@@ -1,30 +1,131 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
include Trello
|
4
|
+
include Trello::Authorization
|
5
|
+
|
6
|
+
describe Client, "and how it handles authorization" do
|
7
|
+
let (:fake_ok_response) {
|
8
|
+
stub "A fake OK response",
|
9
|
+
:code => 200,
|
10
|
+
:body => "A fake response body"
|
11
|
+
}
|
12
|
+
|
13
|
+
before do
|
14
|
+
TInternet.stub(:execute).and_return fake_ok_response
|
15
|
+
Authorization::AuthPolicy.stub(:authorize) do |request|
|
16
|
+
request
|
8
17
|
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "authorizes before it queries the internet" do
|
21
|
+
AuthPolicy.should_receive(:authorize).once.ordered
|
22
|
+
TInternet.should_receive(:execute).once.ordered
|
23
|
+
|
24
|
+
Client.get "/xxx"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "queries the internet with expanded earl and query parameters" do
|
28
|
+
expected_uri = Addressable::URI.parse("https://api.trello.com/1/xxx?a=1&b=2")
|
29
|
+
expected_request = Request.new :get, expected_uri, {}
|
30
|
+
|
31
|
+
TInternet.should_receive(:execute).once.with expected_request
|
32
|
+
|
33
|
+
Client.get "/xxx", :a => "1", :b => "2"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "encodes parameters" do
|
37
|
+
expected_uri = Addressable::URI.parse("https://api.trello.com/1/xxx?name=Jazz%20Kang")
|
38
|
+
expected_request = Request.new :get, expected_uri, {}
|
39
|
+
|
40
|
+
TInternet.should_receive(:execute).once.with expected_request
|
9
41
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
42
|
+
Client.get "/xxx", :name => "Jazz Kang"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "raises an error when response has non-200 status" do
|
46
|
+
expected_error_message = "An error response"
|
47
|
+
response_with_non_200_status = stub "A fake OK response",
|
48
|
+
:code => 201,
|
49
|
+
:body => expected_error_message
|
50
|
+
|
51
|
+
TInternet.stub(:execute).and_return response_with_non_200_status
|
52
|
+
|
53
|
+
lambda{Client.get "/xxx"}.should raise_error expected_error_message
|
54
|
+
end
|
55
|
+
|
56
|
+
it "uses version 1 of the API" do
|
57
|
+
TInternet.should_receive(:execute).once do |request|
|
58
|
+
request.uri.to_s.should =~ /1\//
|
59
|
+
fake_ok_response
|
60
|
+
end
|
14
61
|
|
15
|
-
|
16
|
-
|
17
|
-
lambda { Client.send(:consumer) }.should raise_error(Client::EnterYourPublicKey)
|
18
|
-
end
|
62
|
+
Client.get "/"
|
63
|
+
end
|
19
64
|
|
20
|
-
|
21
|
-
|
22
|
-
|
65
|
+
it "omits the \"?\" when no parameters" do
|
66
|
+
TInternet.should_receive(:execute).once do |request|
|
67
|
+
request.uri.to_s.should_not =~ /\?$/
|
68
|
+
fake_ok_response
|
69
|
+
end
|
70
|
+
|
71
|
+
Client.get "/xxx"
|
72
|
+
end
|
73
|
+
|
74
|
+
it "supports post" do
|
75
|
+
TInternet.should_receive(:execute).once.and_return fake_ok_response
|
76
|
+
|
77
|
+
Client.post "/xxx", { :phil => "T' north" }
|
78
|
+
end
|
23
79
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
80
|
+
it "supplies the body for a post" do
|
81
|
+
expected_body = { :name => "Phil", :nickname => "The Crack Fox" }
|
82
|
+
|
83
|
+
TInternet.should_receive(:execute).once do |request|
|
84
|
+
request.body.should == expected_body
|
85
|
+
fake_ok_response
|
86
|
+
end
|
87
|
+
|
88
|
+
Client.post "/xxx", expected_body
|
89
|
+
end
|
90
|
+
|
91
|
+
it "supplies the path for a post" do
|
92
|
+
expected_path = "/xxx"
|
93
|
+
|
94
|
+
TInternet.should_receive(:execute).once do |request|
|
95
|
+
request.uri.path.should =~ /#{expected_path}$/
|
96
|
+
fake_ok_response
|
28
97
|
end
|
98
|
+
|
99
|
+
Client.post "/xxx", {}
|
100
|
+
end
|
101
|
+
|
102
|
+
it "supports put" do
|
103
|
+
expected_path = "/xxx"
|
104
|
+
|
105
|
+
TInternet.should_receive(:execute).once.and_return fake_ok_response
|
106
|
+
|
107
|
+
Client.put "/xxx", { :phil => "T' north" }
|
108
|
+
end
|
109
|
+
|
110
|
+
it "supplies the body for a put" do
|
111
|
+
expected_body = { :name => "Phil", :nickname => "The Crack Fox" }
|
112
|
+
|
113
|
+
TInternet.should_receive(:execute).once do |request|
|
114
|
+
request.body.should == expected_body
|
115
|
+
fake_ok_response
|
116
|
+
end
|
117
|
+
|
118
|
+
Client.put "/xxx", expected_body
|
119
|
+
end
|
120
|
+
|
121
|
+
it "supplies the path for a put" do
|
122
|
+
expected_path = "/xxx"
|
123
|
+
|
124
|
+
TInternet.should_receive(:execute).once do |request|
|
125
|
+
request.uri.path.should =~ /#{expected_path}$/
|
126
|
+
fake_ok_response
|
127
|
+
end
|
128
|
+
|
129
|
+
Client.put "/xxx", {}
|
29
130
|
end
|
30
|
-
end
|
131
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'integration/integration_test'
|
3
|
+
|
4
|
+
describe "Authorizing read-only requests", :broken => true do
|
5
|
+
include IntegrationTest
|
6
|
+
|
7
|
+
it "Reading public resources requires just a developer public key" do
|
8
|
+
uri = Addressable::URI.parse("https://api.trello.com/1/boards/4ed7e27fe6abb2517a21383d")
|
9
|
+
uri.query_values = {
|
10
|
+
:key => @developer_public_key
|
11
|
+
}
|
12
|
+
|
13
|
+
get(uri).code.should === 200
|
14
|
+
end
|
15
|
+
|
16
|
+
it "Reading private resources requires developer public key AND a member token" do
|
17
|
+
uri = Addressable::URI.parse("https://api.trello.com/1/boards/#{@welcome_board}")
|
18
|
+
uri.query_values = {
|
19
|
+
:key => @developer_public_key,
|
20
|
+
:token => @member_token
|
21
|
+
}
|
22
|
+
|
23
|
+
get(uri).code.should === 200
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can fetch the welcome board" do
|
27
|
+
BasicAuthPolicy.developer_public_key = @developer_public_key
|
28
|
+
BasicAuthPolicy.member_token = @member_token
|
29
|
+
|
30
|
+
Container.set Trello::Authorization, "AuthPolicy", BasicAuthPolicy
|
31
|
+
|
32
|
+
welcome_board = Board.find @welcome_board
|
33
|
+
welcome_board.name.should === "Welcome Board"
|
34
|
+
welcome_board.id.should === @welcome_board
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "OAuth", :broken => true do
|
39
|
+
include IntegrationTest
|
40
|
+
|
41
|
+
before do
|
42
|
+
Container.set Trello::Authorization, "AuthPolicy", OAuthPolicy
|
43
|
+
end
|
44
|
+
|
45
|
+
it "[!] actually does not enforce signature at all, only the keys are required" do
|
46
|
+
OAuthPolicy.consumer_credential = OAuthCredential.new @developer_public_key, nil
|
47
|
+
OAuthPolicy.token = OAuthCredential.new @access_token_key, nil
|
48
|
+
|
49
|
+
pending "I would expect this to fail because I have signed with nil secrets" do
|
50
|
+
lambda{Client.get("/boards/#{@welcome_board}/")}.should raise_error
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'integration/integration_test'
|
3
|
+
|
4
|
+
describe "how to use boards", :broken => true do
|
5
|
+
include IntegrationTest
|
6
|
+
|
7
|
+
context "given a valid access token" do
|
8
|
+
before :all do
|
9
|
+
OAuthPolicy.consumer_credential = OAuthCredential.new @developer_public_key, @developer_secret
|
10
|
+
OAuthPolicy.token = OAuthCredential.new @access_token_key, @access_token_secret
|
11
|
+
Container.set Trello::Authorization, "AuthPolicy", OAuthPolicy
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
if @new_board and false == @new_board.closed?
|
16
|
+
@new_board.update_fields 'closed' => true
|
17
|
+
@new_board.save!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can add a board" do
|
22
|
+
@new_board = Board.create(:name => "An example")
|
23
|
+
@new_board.should_not be_nil
|
24
|
+
@new_board.id.should_not be_nil
|
25
|
+
@new_board.name.should == "An example"
|
26
|
+
@new_board.should_not be_closed
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can read the welcome board" do
|
30
|
+
welcome_board = Board.find @welcome_board
|
31
|
+
welcome_board.name.should === "Welcome Board"
|
32
|
+
welcome_board.id.should === @welcome_board
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can close a board" do
|
36
|
+
@new_board = Board.create(:name => "[#{Time.now}, CLOSED] An example")
|
37
|
+
|
38
|
+
@new_board.update_fields 'closed' => true
|
39
|
+
@new_board.save!
|
40
|
+
|
41
|
+
Board.find(@new_board.id).should be_closed
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can list all boards" do
|
45
|
+
Client.get("/members/me/boards/").json_into(Board).should be_an Array
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module IntegrationTest
|
2
|
+
include Trello
|
3
|
+
include Trello::Authorization
|
4
|
+
|
5
|
+
class Container
|
6
|
+
class << self
|
7
|
+
def set(parent, name, value)
|
8
|
+
parent.send :remove_const, name
|
9
|
+
parent.const_set name, value
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(klass)
|
15
|
+
klass.class_eval do
|
16
|
+
before :all do
|
17
|
+
# Getting developer/member key
|
18
|
+
# 1. https://trello.com/1/appKey/generate
|
19
|
+
# 2. https://trello.com/1/connect?key=<public_key_here>&name=RubyTrelloIntegrationTests&response_type=token
|
20
|
+
# See: https://trello.com/board/trello-public-api/4ed7e27fe6abb2517a21383d
|
21
|
+
|
22
|
+
@developer_public_key = ENV["DEVELOPER_PUBLIC_KEY"]
|
23
|
+
@developer_secret = ENV["DEVELOPER_SECRET"]
|
24
|
+
@member_token = ENV["MEMBER_TOKEN"]
|
25
|
+
@welcome_board = ENV["WELCOME_BOARD"]
|
26
|
+
@access_token_key = ENV["ACCESS_TOKEN_KEY"]
|
27
|
+
@access_token_secret = ENV["ACCESS_TOKEN_SECRET"]
|
28
|
+
|
29
|
+
WebMock.disable!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def get(uri)
|
37
|
+
require "rest_client"
|
38
|
+
RestClient.get uri.to_s
|
39
|
+
end
|
40
|
+
end
|
data/spec/list_spec.rb
CHANGED
@@ -4,14 +4,9 @@ module Trello
|
|
4
4
|
describe List do
|
5
5
|
include Helpers
|
6
6
|
|
7
|
-
before(:all) do
|
8
|
-
Client.public_key = 'dummy'
|
9
|
-
Client.secret = 'dummy'
|
10
|
-
end
|
11
|
-
|
12
7
|
before(:each) do
|
13
|
-
|
14
|
-
|
8
|
+
Client.stub(:get).with("/lists/abcdef123456789123456789").and_return JSON.generate(lists_details.first)
|
9
|
+
Client.stub(:get).with("/boards/abcdef123456789123456789").and_return JSON.generate(boards_details.first)
|
15
10
|
|
16
11
|
@list = List.find("abcdef123456789123456789")
|
17
12
|
end
|
@@ -34,11 +29,22 @@ module Trello
|
|
34
29
|
end
|
35
30
|
end
|
36
31
|
|
32
|
+
context "actions" do
|
33
|
+
it "has a list of actions" do
|
34
|
+
Client.stub(:get).with("/lists/abcdef123456789123456789/actions").and_return actions_payload
|
35
|
+
@list.actions.count.should be > 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
37
39
|
context "cards" do
|
38
40
|
it "has a list of cards" do
|
39
|
-
|
41
|
+
Client.stub(:get).with("/lists/abcdef123456789123456789/cards", { :filter => :open }).and_return cards_payload
|
40
42
|
@list.cards.count.should be > 0
|
41
43
|
end
|
42
44
|
end
|
45
|
+
|
46
|
+
it "is not closed" do
|
47
|
+
@list.closed?.should_not be_true
|
48
|
+
end
|
43
49
|
end
|
44
|
-
end
|
50
|
+
end
|
data/spec/member_spec.rb
CHANGED
@@ -6,35 +6,38 @@ module Trello
|
|
6
6
|
describe Member do
|
7
7
|
include Helpers
|
8
8
|
|
9
|
-
before(:all) do
|
10
|
-
Client.public_key = 'dummy'
|
11
|
-
Client.secret = 'dummy'
|
12
|
-
end
|
13
|
-
|
14
9
|
before(:each) do
|
15
|
-
|
10
|
+
Client.stub(:get).with("/members/me").and_return user_payload
|
16
11
|
|
17
12
|
@member = Member.find('me')
|
18
13
|
end
|
19
14
|
|
20
15
|
context "actions" do
|
21
|
-
it "retrieves a list of actions" do
|
22
|
-
|
16
|
+
it "retrieves a list of actions", :refactor => true do
|
17
|
+
Client.stub(:get).with("/members/me/actions").and_return actions_payload
|
23
18
|
@member.actions.count.should be > 0
|
24
19
|
end
|
25
20
|
end
|
26
21
|
|
27
22
|
context "boards" do
|
28
23
|
it "has a list of boards" do
|
29
|
-
|
24
|
+
Client.stub(:get).with("/members/me/boards", { :filter => :all }).and_return boards_payload
|
30
25
|
boards = @member.boards
|
31
26
|
boards.count.should be > 0
|
32
27
|
end
|
33
28
|
end
|
34
29
|
|
30
|
+
context "cards" do
|
31
|
+
it "has a list of cards" do
|
32
|
+
Client.stub(:get).with("/members/me/cards", { :filter => :open }).and_return cards_payload
|
33
|
+
cards = @member.cards
|
34
|
+
cards.count.should be > 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
35
38
|
context "organizations" do
|
36
39
|
it "has a list of organizations" do
|
37
|
-
|
40
|
+
Client.stub(:get).with("/members/me/organizations", { :filter => :all }).and_return orgs_payload
|
38
41
|
orgs = @member.organizations
|
39
42
|
orgs.count.should be > 0
|
40
43
|
end
|
@@ -62,4 +65,4 @@ module Trello
|
|
62
65
|
end
|
63
66
|
end
|
64
67
|
end
|
65
|
-
end
|
68
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
include Trello::Authorization
|
4
|
+
include Trello
|
5
|
+
|
6
|
+
describe OAuthPolicy do
|
7
|
+
before do
|
8
|
+
OAuthPolicy.consumer_credential = OAuthCredential.new "xxx", "xxx"
|
9
|
+
OAuthPolicy.token = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "adds an authorization header" do
|
13
|
+
uri = Addressable::URI.parse("https://xxx/")
|
14
|
+
|
15
|
+
request = Request.new :get, uri
|
16
|
+
|
17
|
+
authorized_request = OAuthPolicy.authorize request
|
18
|
+
|
19
|
+
authorized_request.headers.keys.should include "Authorization"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "preserves query parameters" do
|
23
|
+
uri = Addressable::URI.parse("https://xxx/?name=Riccardo")
|
24
|
+
request = Request.new :get, uri
|
25
|
+
|
26
|
+
authorized_request = OAuthPolicy.authorize request
|
27
|
+
|
28
|
+
the_query_parameters = Addressable::URI.parse(authorized_request.uri).query_values
|
29
|
+
the_query_parameters.should == {"name" => "Riccardo"}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "adds the correct signature as part of authorization header" do
|
33
|
+
Clock.stub(:timestamp).and_return "1327048592"
|
34
|
+
Nonce.stub(:next).and_return "b94ff2bf7f0a5e87a326064ae1dbb18f"
|
35
|
+
|
36
|
+
OAuthPolicy.consumer_credential = OAuthCredential.new "consumer_key", "consumer_secret"
|
37
|
+
|
38
|
+
request = Request.new :get, Addressable::URI.parse("http://xxx/")
|
39
|
+
|
40
|
+
authorized_request = OAuthPolicy.authorize request
|
41
|
+
|
42
|
+
authorized_request.headers["Authorization"].should =~ /oauth_signature="u7CmId4WEDUqPdHnWVf1JVChFmg%3D"/
|
43
|
+
end
|
44
|
+
|
45
|
+
it "adds correct signature for uri with parameters" do
|
46
|
+
Clock.stub(:timestamp).and_return "1327351010"
|
47
|
+
Nonce.stub(:next).and_return "f5474aaf44ca84df0b09870044f91c69"
|
48
|
+
|
49
|
+
OAuthPolicy.consumer_credential = OAuthCredential.new "consumer_key", "consumer_secret"
|
50
|
+
|
51
|
+
request = Request.new :get, Addressable::URI.parse("http://xxx/?a=b")
|
52
|
+
|
53
|
+
authorized_request = OAuthPolicy.authorize request
|
54
|
+
|
55
|
+
authorized_request.headers["Authorization"].should =~ /oauth_signature="ABL%2FcOSGJSbvvLt1gW2nV9i%2FDyA%3D"/
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails if consumer_credential is unset" do
|
59
|
+
OAuthPolicy.consumer_credential = nil
|
60
|
+
|
61
|
+
request = Request.new :get, Addressable::URI.parse("http://xxx/")
|
62
|
+
|
63
|
+
lambda{OAuthPolicy.authorize request}.should raise_error "The consumer_credential has not been supplied."
|
64
|
+
end
|
65
|
+
|
66
|
+
it "can sign with token" do
|
67
|
+
Clock.stub(:timestamp).and_return "1327360530"
|
68
|
+
Nonce.stub(:next).and_return "4f610cb28e7aa8711558de5234af1f0e"
|
69
|
+
|
70
|
+
OAuthPolicy.consumer_credential = OAuthCredential.new "consumer_key", "consumer_secret"
|
71
|
+
OAuthPolicy.token = OAuthCredential.new "token_key", "token_secret"
|
72
|
+
|
73
|
+
request = Request.new :get, Addressable::URI.parse("http://xxx/")
|
74
|
+
|
75
|
+
authorized_request = OAuthPolicy.authorize request
|
76
|
+
|
77
|
+
authorized_request.headers["Authorization"].should =~ /oauth_signature="1Boj4fo6KiXA4xGD%2BKF5QOD36PI%3D"/
|
78
|
+
end
|
79
|
+
|
80
|
+
it "adds correct signature for https uri"
|
81
|
+
it "adds correct signature for verbs other than get"
|
82
|
+
it "can sign with access token too"
|
83
|
+
end
|