passaporteweb-client 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,118 @@
1
+ # encoding: utf-8
2
+ module PassaporteWeb
3
+
4
+ # Represents a ServiceAccount on PassaporteWeb, which is the 'account' of an Identity within a Service. A
5
+ # Service may have many ServiceAccount s and many Identity ies via it's ServiceAccount s. A Identity may
6
+ # belong to serveral Service s via it's ServiceAccount s.
7
+ class ServiceAccount
8
+ include Attributable
9
+
10
+ ATTRIBUTES = [:plan_slug, :expiration, :identity, :roles, :member_uuid, :role, :include_expired_accounts, :name, :members_data, :url, :service_data, :account_data, :add_member_url]
11
+ UPDATABLE_ATTRIBUTES = [:plan_slug, :expiration]
12
+
13
+ attr_accessor *UPDATABLE_ATTRIBUTES
14
+ attr_reader *(ATTRIBUTES - UPDATABLE_ATTRIBUTES)
15
+ attr_reader :errors
16
+
17
+ def initialize(attributes={})
18
+ set_attributes(attributes)
19
+ @errors = {}
20
+ end
21
+
22
+ def uuid
23
+ self.account_data['uuid'] if self.account_data
24
+ end
25
+
26
+ # Finds all ServiceAccounts that the current authenticated application has access to, paginated. By default finds
27
+ # 20 ServiceAccounts per request, starting at "page" 1. Returns an OpenStruct object with two attributes
28
+ # <tt>service_accounts</tt> and <tt>meta</tt>. <tt>service_accounts</tt> is an array of ServiceAccount instances or an empty array
29
+ # if no ServiceAccounts are found. <tt>meta</tt> is an OpenStruct object with information about limit and available
30
+ # pagination values, to use in subsequent calls to <tt>.find_all</tt>. Raises a
31
+ # <tt>RestClient::ResourceNotFound</tt> exception if the requested page does not exist.
32
+ #
33
+ # API method: <tt>GET /organizations/api/accounts/</tt>
34
+ #
35
+ # API documentation: https://app.passaporteweb.com.br/static/docs/account_manager.html#get-organizations-api-accounts
36
+ #
37
+ # Example:
38
+ # data = PassaporteWeb::ServiceAccount.find_all
39
+ # data.service_accounts # => [account1, account2, ...]
40
+ # data.meta # => #<OpenStruct limit=20, next_page=2, prev_page=nil, first_page=1, last_page=123>
41
+ # data.meta.limit # => 20
42
+ # data.meta.next_page # => 2
43
+ # data.meta.prev_page # => nil
44
+ # data.meta.first_page # => 1
45
+ # data.meta.last_page # => 123
46
+ def self.find_all(page=1, limit=20)
47
+ response = Http.get("/organizations/api/accounts/?page=#{Integer(page)}&limit=#{Integer(limit)}")
48
+ raw_accounts = MultiJson.decode(response.body)
49
+ result_hash = {}
50
+ result_hash[:service_accounts] = raw_accounts.map { |raw_account| load_service_account(raw_account) }
51
+ result_hash[:meta] = PassaporteWeb::Helpers.meta_links_from_header(response.headers[:link])
52
+ PassaporteWeb::Helpers.convert_to_ostruct_recursive(result_hash)
53
+ end
54
+
55
+ # Instanciates an ServiceAccount identified by it's UUID, with all the details. Only service accounts related to the current
56
+ # authenticated application are available. Returns the ServiceAccount instance if successful, or raises a
57
+ # <tt>RestClient::ResourceNotFound</tt> exception if no ServiceAccount exists with that UUID (or if it is not
58
+ # related to the current authenticated application).
59
+ #
60
+ # API method: <tt>GET /organizations/api/accounts/:uuid/</tt>
61
+ #
62
+ # API documentation: https://app.passaporteweb.com.br/static/docs/account_manager.html#get-organizations-api-accounts-uuid
63
+ def self.find(uuid)
64
+ response = Http.get("/organizations/api/accounts/#{uuid}/")
65
+ attributes_hash = MultiJson.decode(response.body)
66
+ load_service_account(attributes_hash)
67
+ end
68
+
69
+ # Updates an existing ServiceAccount, changing it's plan_slug and/or expiration date. Returns true
70
+ # if successfull or false if not. In case of failure, it will fill the <tt>errors</tt> attribute
71
+ # with the reason for the failure to save the object.
72
+ #
73
+ # API method: <tt>PUT /organizations/api/accounts/:uuid/</tt>
74
+ #
75
+ # API documentation: https://app.passaporteweb.com.br/static/docs/account_manager.html#put-organizations-api-accounts-uuid
76
+ def save
77
+ # TODO validar atributos?
78
+ response = update
79
+ raise "unexpected response: #{response.code} - #{response.body}" unless response.code == 200
80
+ attributes_hash = MultiJson.decode(response.body)
81
+ set_attributes(attributes_hash)
82
+ @errors = {}
83
+ true
84
+ rescue *[RestClient::Conflict, RestClient::BadRequest] => e
85
+ @errors = MultiJson.decode(e.response.body)
86
+ false
87
+ end
88
+
89
+ def attributes
90
+ ATTRIBUTES.inject({}) do |hash, attribute|
91
+ hash[attribute] = self.send(attribute)
92
+ hash
93
+ end
94
+ end
95
+
96
+ def persisted?
97
+ @persisted == true
98
+ end
99
+
100
+ private
101
+
102
+ def update
103
+ Http.put("/organizations/api/accounts/#{self.uuid}/", update_body)
104
+ end
105
+
106
+ def update_body
107
+ self.attributes.select { |key, value| UPDATABLE_ATTRIBUTES.include?(key) && !value.nil? }
108
+ end
109
+
110
+ def self.load_service_account(attributes_hash)
111
+ service_account = self.new(attributes_hash)
112
+ service_account.instance_variable_set(:@persisted, true)
113
+ service_account
114
+ end
115
+
116
+ end
117
+
118
+ end
@@ -0,0 +1,136 @@
1
+ # encoding: utf-8
2
+ module PassaporteWeb
3
+
4
+ # Represents the membership of an Identity within a ServiceAccount on PassaporteWeb.
5
+ class ServiceAccountMember
6
+
7
+ attr_accessor :roles
8
+ attr_reader :service_account, :identity
9
+ attr_reader :errors
10
+
11
+ # Instanciates a new ServiceAccountMember to represent the membership of the supplied Identity in
12
+ # the supplied ServiceAccount. The <tt>roles</tt> attribute should be an array of strings. Any
13
+ # value is accepted, only 'owner' is reserved. By default uses only the value of 'user'.
14
+ def initialize(service_account, identity, roles=['user'])
15
+ @service_account = service_account
16
+ @identity = identity
17
+ @roles = roles
18
+ @membership_details_url = nil
19
+ @errors = {}
20
+ end
21
+
22
+ # Finds the membership relation between the supplied Identity in the supplied ServiceAccount and returns
23
+ # an instance of ServiceAccountMember representing it.
24
+ #
25
+ # Raises a <tt>RestClient::ResourceNotFound</tt> exception if the supplied Identity does not have a
26
+ # membership within the supplied ServiceAccount, or if any of the supplied objects is invalid or
27
+ # not existent.
28
+ #
29
+ # API method: <tt>GET /organizations/api/accounts/:uuid/members/:member_uuid/</tt>
30
+ #
31
+ # API documentation: https://app.passaporteweb.com.br/static/docs/account_manager.html#get-organizations-api-accounts-uuid-members-member-uuid
32
+ def self.find(service_account, identity)
33
+ response = Http.get("/organizations/api/accounts/#{service_account.uuid}/members/#{identity.uuid}/")
34
+ raise "unexpected response: #{response.code} - #{response.body}" unless response.code == 200
35
+ attributes_hash = MultiJson.decode(response.body)
36
+ member = self.new(service_account, identity, attributes_hash['roles'])
37
+ member.instance_variable_set(:@persisted, true)
38
+ member.instance_variable_set(:@destroyed, false)
39
+ member
40
+ end
41
+
42
+ # Returns true if the ServiceAccountMember was loaded from PassaporteWeb or saved there successfully.
43
+ def persisted?
44
+ @persisted == true
45
+ end
46
+
47
+ # Returns true if the ServiceAccountMember object has been destroyed (and thus represents a membership
48
+ # no more valid on PassaporteWeb)
49
+ def destroyed?
50
+ @destroyed == true
51
+ end
52
+
53
+ def membership_details_url # :nodoc:
54
+ return @membership_details_url if @membership_details_url
55
+ "/organizations/api/accounts/#{self.service_account.uuid}/members/#{self.identity.uuid}/" if persisted?
56
+ end
57
+
58
+ # Creates or updates the ServiceAccountMember object on PassaporteWeb. Returns true if successful and false
59
+ # (along with the reason for failure in the #errors method) otherwise.
60
+ #
61
+ # On update, the only attribute that can be changed is the <tt>roles</tt>. This can be set to anything,
62
+ # including 'owner', on update (on create, the 'owner' role is not allowed.)
63
+ #
64
+ # API methods:
65
+ # * <tt>POST /organizations/api/accounts/:uuid/members/</tt> (on create)
66
+ # * <tt>PUT /organizations/api/accounts/:uuid/members/:member_uuid/</tt> (on update)
67
+ #
68
+ # API documentation:
69
+ # * https://app.passaporteweb.com.br/static/docs/account_manager.html#post-organizations-api-accounts-uuid-members
70
+ # * https://app.passaporteweb.com.br/static/docs/account_manager.html#put-organizations-api-accounts-uuid-members-member-uuid
71
+ def save
72
+ self.persisted? ? update : create
73
+ end
74
+
75
+ # Destroys the membership relation between a Identity and a ServiceAccount, e.g. the Identity no longer will be a
76
+ # member of the ServiceAccount. Returns true if successful of false (along with the reason for failure in the #errors
77
+ # method) otherwise.
78
+ #
79
+ # If the member has a 'owner' role, than the membership can not be destroyed.
80
+ #
81
+ # API method: <tt>DELETE /organizations/api/accounts/:uuid/members/:member_uuid/</tt>
82
+ #
83
+ # API documentation: https://app.passaporteweb.com.br/static/docs/account_manager.html#delete-organizations-api-accounts-uuid-members-member-uuid
84
+ def destroy
85
+ return false unless self.persisted?
86
+ response = Http.delete(self.membership_details_url)
87
+ raise "unexpected response: #{response.code} - #{response.body}" unless response.code == 204
88
+ @errors = {}
89
+ @persisted = false
90
+ @destroyed = true
91
+ true
92
+ rescue *[RestClient::NotAcceptable] => e
93
+ @errors = MultiJson.decode(e.response.body)
94
+ @persisted = true
95
+ @destroyed = false
96
+ false
97
+ end
98
+
99
+ private
100
+
101
+ def create
102
+ response = Http.post(
103
+ "/organizations/api/accounts/#{self.service_account.uuid}/members/",
104
+ {identity: self.identity.uuid, roles: self.roles}
105
+ )
106
+ raise "unexpected response: #{response.code} - #{response.body}" unless response.code == 200 # doc says 201
107
+ attributes_hash = MultiJson.decode(response.body)
108
+ @membership_details_url = attributes_hash['membership_details_url']
109
+ @errors = {}
110
+ @persisted = true
111
+ true
112
+ rescue *[RestClient::Conflict, RestClient::BadRequest, RestClient::NotAcceptable] => e
113
+ @errors = MultiJson.decode(e.response.body)
114
+ @persisted = false
115
+ false
116
+ end
117
+
118
+ def update
119
+ response = Http.put(
120
+ self.membership_details_url,
121
+ {roles: self.roles}
122
+ )
123
+ raise "unexpected response: #{response.code} - #{response.body}" unless response.code == 200
124
+ attributes_hash = MultiJson.decode(response.body)
125
+ @errors = {}
126
+ @persisted = true
127
+ true
128
+ rescue *[RestClient::BadRequest, RestClient::NotAcceptable] => e
129
+ @errors = MultiJson.decode(e.response.body)
130
+ @persisted = true
131
+ false
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ module PassaporteWeb
3
+ VERSION = "0.0.10"
4
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ # Ensure we require the local version and not one we might have installed already
6
+ require File.join([File.dirname(__FILE__),'lib','passaporte_web','version.rb'])
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "passaporteweb-client"
10
+ spec.version = PassaporteWeb::VERSION
11
+ spec.authors = ["Rodrigo Tassinari de Oliveira", "Rodrigo Martins"]
12
+ spec.email = ["rodrigo@pittlandia.net", "rodrigo.tassinari@myfreecomm.com.br", "rodrigo.martins@myfreecomm.com.br", "rodrigo@rrmartins.com"]
13
+ spec.description = %q{A Ruby client for the PassaporteWeb REST API}
14
+ spec.summary = %q{A Ruby client for the PassaporteWeb REST API: https://app.passaporteweb.com.br/static/docs/}
15
+ spec.homepage = "https://github.com/myfreecomm/passaporteweb-client-ruby"
16
+ spec.license = "Apache-v2"
17
+ spec.has_rdoc = true
18
+
19
+ # VCR cassettes are too long for the gemspec, see http://stackoverflow.com/questions/14371686/building-rails-3-engine-throwing-gempackagetoolongfilename-error
20
+ # spec.files = `git ls-files`.split($/)
21
+ spec.files = `git ls-files`.split($/).reject { |f| f =~ %r{(vcr_cassettes)/} }
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "rest-client", "~> 1.6.7"
27
+ spec.add_dependency "multi_json", "~> 1.7.1"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.3.2"
30
+ spec.add_development_dependency "rake", "~> 10.0.4"
31
+ spec.add_development_dependency 'rdoc', '~> 4.0.1'
32
+ spec.add_development_dependency "rspec", "~> 2.13.0"
33
+ spec.add_development_dependency "vcr", "~> 2.4.0"
34
+ spec.add_development_dependency "webmock", "~> 1.9.3"
35
+ spec.add_development_dependency "pry", "~> 0.9.12"
36
+ spec.add_development_dependency "pry-nav", "~> 0.2.3"
37
+ spec.add_development_dependency "awesome_print", "~> 1.1.0"
38
+ spec.add_development_dependency "simplecov", "~> 0.7.1"
39
+ spec.add_development_dependency "coveralls", "~> 0.6.3"
40
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe PassaporteWeb::Configuration do
5
+
6
+ it "should use the production PassaporteWeb URL by default" do
7
+ PassaporteWeb::Configuration.new.url.should == 'https://app.passaporteweb.com.br'
8
+ end
9
+
10
+ it "should use a default user agent" do
11
+ PassaporteWeb::Configuration.new.user_agent.should == "PassaporteWeb Ruby Client v#{PassaporteWeb::VERSION}"
12
+ end
13
+
14
+ it 'should allow setting the configuration parameters' do
15
+ config = PassaporteWeb::Configuration.new
16
+
17
+ config.url = 'http://sandbox.app.passaporteweb.com.br'
18
+ config.application_token = 'some-app-token'
19
+ config.application_secret = 'some-app-secret'
20
+
21
+ config.url.should == 'http://sandbox.app.passaporteweb.com.br'
22
+ config.application_token.should == 'some-app-token'
23
+ config.application_secret.should == 'some-app-secret'
24
+ config.user_token.should be_nil
25
+ end
26
+
27
+ describe "#application_credentials" do
28
+ let(:config) { PassaporteWeb::Configuration.new }
29
+ it "should return the HTTP Basic Auth header value for the application login" do
30
+ config.application_token = 'some-app-token'
31
+ config.application_secret = 'some-app-secret'
32
+ config.application_credentials.should == 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0'
33
+ end
34
+ it "should require the application_token to be set" do
35
+ config.application_secret = 'some-app-secret'
36
+ expect { config.application_credentials }.to raise_error(ArgumentError, 'application_token not set')
37
+ end
38
+ it "should require the application_secret to be set" do
39
+ config.application_token = 'some-app-token'
40
+ expect { config.application_credentials }.to raise_error(ArgumentError, 'application_secret not set')
41
+ end
42
+ end
43
+
44
+ describe "#user_credentials" do
45
+ let(:config) { PassaporteWeb::Configuration.new }
46
+ it "should return the HTTP Basic Auth header value for the user login" do
47
+ config.user_token = 'some-user-token'
48
+ config.user_credentials.should == 'Basic OnNvbWUtdXNlci10b2tlbg=='
49
+ end
50
+ it "should require the user_token to be set" do
51
+ expect { config.user_credentials }.to raise_error(ArgumentError, 'user_token not set')
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe PassaporteWeb::Helpers do
5
+
6
+ describe ".meta_links_from_header" do
7
+ it "should return a hash from the link header string" do
8
+ link_header = "<http://sandbox.app.passaporteweb.com.br/organizations/api/accounts/?page=3&limit=3>; rel=next, <http://sandbox.app.passaporteweb.com.br/organizations/api/accounts/?page=1&limit=3>; rel=prev, <http://sandbox.app.passaporteweb.com.br/organizations/api/accounts/?page=123&limit=3>; rel=last, <http://sandbox.app.passaporteweb.com.br/organizations/api/accounts/?page=1&limit=3>; rel=first"
9
+ described_class.meta_links_from_header(link_header).should == {
10
+ limit: 3,
11
+ next_page: 3,
12
+ prev_page: 1,
13
+ first_page: 1,
14
+ last_page: 123
15
+ }
16
+ end
17
+ end
18
+
19
+ describe ".convert_to_ostruct_recursive" do
20
+ it "should convert a hash recursevely to a OpenStruct" do
21
+ hash = {a: 1, b: {c: 3, d: 4, e: {f: 6, g: 7}}}
22
+ os = described_class.convert_to_ostruct_recursive(hash)
23
+ os.should be_instance_of(OpenStruct)
24
+ os.a.should == 1
25
+ os.b.should be_instance_of(OpenStruct)
26
+ os.b.c.should == 3
27
+ os.b.d.should == 4
28
+ os.b.e.should be_instance_of(OpenStruct)
29
+ os.b.e.f.should == 6
30
+ os.b.e.g.should == 7
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,114 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe PassaporteWeb::Http do
5
+
6
+ before(:each) do
7
+ PassaporteWeb.configure do |c|
8
+ c.url = 'https://some/where'
9
+ c.user_agent = 'My Mocking App v1.1'
10
+ c.application_token = 'some-app-token'
11
+ c.application_secret = 'some-app-secret'
12
+ c.user_token = 'some-user-token'
13
+ end
14
+ end
15
+
16
+ let(:mock_response) { mock('restclient http response') }
17
+
18
+ describe ".get" do
19
+ it "should use RestClient.get with the supplied params and common options" do
20
+ RestClient.should_receive(:get).with(
21
+ 'https://some/where/foo',
22
+ params: {spam: 'eggs'},
23
+ authorization: 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0',
24
+ content_type: :json,
25
+ accept: :json,
26
+ user_agent: 'My Mocking App v1.1'
27
+ ).and_return(mock_response)
28
+ described_class.get('/foo', spam: 'eggs')
29
+ end
30
+ end
31
+
32
+ describe ".custom_auth_get" do
33
+ it "should use RestClient.get with the supplied params and common options, but with the custom authorization" do
34
+ RestClient.should_receive(:get).with(
35
+ 'https://some/where/foo',
36
+ params: {spam: 'eggs'},
37
+ authorization: 'Basic am9obkBkb2UuY29tOnRoZWJpcmRpc3RoZXdvcmQ=',
38
+ content_type: :json,
39
+ accept: :json,
40
+ user_agent: 'My Mocking App v1.1'
41
+ ).and_return(mock_response)
42
+ described_class.custom_auth_get('john@doe.com', 'thebirdistheword', '/foo', spam: 'eggs')
43
+ end
44
+ end
45
+
46
+ describe ".delete" do
47
+ it "should use RestClient.delete with the supplied params and common options" do
48
+ RestClient.should_receive(:delete).with(
49
+ 'https://some/where/foo',
50
+ params: {spam: 'eggs'},
51
+ authorization: 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0',
52
+ content_type: :json,
53
+ accept: :json,
54
+ user_agent: 'My Mocking App v1.1'
55
+ ).and_return(mock_response)
56
+ described_class.delete('/foo', spam: 'eggs')
57
+ end
58
+ end
59
+
60
+ describe ".put" do
61
+ it "should use RestClient.get with the supplied params and common options, encoding body as json" do
62
+ RestClient.should_receive(:put).with(
63
+ 'https://some/where/foo',
64
+ '{"hello":"world"}',
65
+ params: {spam: 'eggs'},
66
+ authorization: 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0',
67
+ content_type: :json,
68
+ accept: :json,
69
+ user_agent: 'My Mocking App v1.1'
70
+ ).and_return(mock_response)
71
+ described_class.put('/foo', {hello: 'world'}, {spam: 'eggs'})
72
+ end
73
+ it "should use RestClient.get with the supplied params and common options, with body already as json" do
74
+ RestClient.should_receive(:put).with(
75
+ 'https://some/where/foo',
76
+ '{"hello":"world"}',
77
+ params: {spam: 'eggs'},
78
+ authorization: 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0',
79
+ content_type: :json,
80
+ accept: :json,
81
+ user_agent: 'My Mocking App v1.1'
82
+ ).and_return(mock_response)
83
+ described_class.put('/foo', '{"hello":"world"}', {spam: 'eggs'})
84
+ end
85
+ end
86
+
87
+ describe ".post" do
88
+ it "should use RestClient.post with the supplied params and common options, encoding body as json" do
89
+ RestClient.should_receive(:post).with(
90
+ 'https://some/where/foo',
91
+ '{"hello":"world"}',
92
+ params: {},
93
+ authorization: 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0',
94
+ content_type: :json,
95
+ accept: :json,
96
+ user_agent: 'My Mocking App v1.1'
97
+ ).and_return(mock_response)
98
+ described_class.post('/foo', {hello: 'world'})
99
+ end
100
+ it "should use RestClient.post with the supplied params and common options, with body already as json" do
101
+ RestClient.should_receive(:post).with(
102
+ 'https://some/where/foo',
103
+ '{"hello":"world"}',
104
+ params: {},
105
+ authorization: 'Basic c29tZS1hcHAtdG9rZW46c29tZS1hcHAtc2VjcmV0',
106
+ content_type: :json,
107
+ accept: :json,
108
+ user_agent: 'My Mocking App v1.1'
109
+ ).and_return(mock_response)
110
+ described_class.post('/foo', '{"hello":"world"}')
111
+ end
112
+ end
113
+
114
+ end