google_apps_oauth2 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +10 -0
- data/README.md +461 -0
- data/Rakefile +5 -0
- data/google_apps_oauth2.gemspec +20 -0
- data/lib/google_apps_oauth2.rb +7 -0
- data/lib/google_apps_oauth2/atom/atom.rb +26 -0
- data/lib/google_apps_oauth2/atom/document.rb +46 -0
- data/lib/google_apps_oauth2/atom/feed.rb +90 -0
- data/lib/google_apps_oauth2/atom/node.rb +57 -0
- data/lib/google_apps_oauth2/atom/user.rb +21 -0
- data/lib/google_apps_oauth2/parsers/feed_parser.rb +15 -0
- data/lib/google_apps_oauth2/transport.rb +57 -0
- data/spec/fixtures/invalid_token.html +10 -0
- data/spec/fixtures/refreshed_token_response.json +6 -0
- data/spec/fixtures/user.xml +5 -0
- data/spec/fixtures/users_feed.xml +49 -0
- data/spec/google_apps_oauth2/atom/document_spec.rb +18 -0
- data/spec/google_apps_oauth2/atom/feed_spec.rb +78 -0
- data/spec/google_apps_oauth2/atom/node_spec.rb +34 -0
- data/spec/google_apps_oauth2/atom/user_spec.rb +41 -0
- data/spec/google_apps_oauth2/parsers/feed_parser_spec.rb +16 -0
- data/spec/google_apps_oauth2/transport_spec.rb +46 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/web_stubs.rb +16 -0
- metadata +153 -0
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Gem::Specification.new do |spec|
|
2
|
+
spec.name = 'google_apps_oauth2'
|
3
|
+
spec.version = '0.1'
|
4
|
+
spec.license = "MIT"
|
5
|
+
spec.summary = 'Google Apps APIs using OAuth2'
|
6
|
+
spec.description = 'Library for interfacing with Google Apps Domain and Application APIs via OAuth2'
|
7
|
+
spec.authors = ['Will Read']
|
8
|
+
spec.files = Dir.glob(File.join('**', 'lib', '**', '*.rb'))
|
9
|
+
spec.homepage = 'https://github.com/TildeWill/google_apps'
|
10
|
+
|
11
|
+
spec.add_dependency('libxml-ruby', '>= 2.2.2')
|
12
|
+
spec.add_dependency('httparty')
|
13
|
+
|
14
|
+
spec.add_development_dependency 'rake'
|
15
|
+
spec.add_development_dependency 'rspec'
|
16
|
+
spec.add_development_dependency 'webmock'
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split("\n")
|
19
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'google_apps_oauth2/parsers/feed_parser'
|
2
|
+
require 'google_apps_oauth2/transport'
|
3
|
+
require 'google_apps_oauth2/atom/atom'
|
4
|
+
require 'google_apps_oauth2/atom/node'
|
5
|
+
require 'google_apps_oauth2/atom/document'
|
6
|
+
require 'google_apps_oauth2/atom/feed'
|
7
|
+
require 'google_apps_oauth2/atom/user'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'libxml'
|
2
|
+
|
3
|
+
module GoogleAppsOauth2
|
4
|
+
module Atom
|
5
|
+
include LibXML
|
6
|
+
|
7
|
+
NAMESPACES = {
|
8
|
+
atom: 'http://www.w3.org/2005/Atom',
|
9
|
+
apps: 'http://schemas.google.com/apps/2006',
|
10
|
+
gd: 'http://schemas.google.com/g/2005',
|
11
|
+
openSearch: 'http://a9.com/-/spec/opensearchrss/1.0/'
|
12
|
+
}
|
13
|
+
|
14
|
+
CATEGORY = {
|
15
|
+
user: [['scheme', 'http://schemas.google.com/g/2005#kind'], ['term', 'http://schemas.google.com/apps/2006#user']],
|
16
|
+
}
|
17
|
+
|
18
|
+
ENTRY_TAG = ["<atom:entry xmlns:atom=\"#{NAMESPACES[:atom]}\" xmlns:apps=\"#{NAMESPACES[:apps]}\" xmlns:gd=\"#{NAMESPACES[:gd]}\">", '</atom:entry>']
|
19
|
+
|
20
|
+
def user(*args)
|
21
|
+
User.new *args
|
22
|
+
end
|
23
|
+
|
24
|
+
module_function :user
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module GoogleAppsOauth2
|
2
|
+
module Atom
|
3
|
+
class Document
|
4
|
+
include Node
|
5
|
+
|
6
|
+
def initialize(doc, map = {})
|
7
|
+
@doc = parse(doc)
|
8
|
+
@map = map
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(xml)
|
12
|
+
document = Atom::XML::Document.string(xml)
|
13
|
+
Atom::XML::Parser.document(document).parse
|
14
|
+
end
|
15
|
+
|
16
|
+
# find_values searches @doc and assigns any values
|
17
|
+
# to their corresponding instance variables. This is
|
18
|
+
# useful when we've been given a string of XML and need
|
19
|
+
# internal consistency in the object.
|
20
|
+
#
|
21
|
+
# find_values
|
22
|
+
def find_values
|
23
|
+
@doc.root.each do |entry|
|
24
|
+
intersect = @map.keys & entry.attributes.to_h.keys.map(&:to_sym)
|
25
|
+
set_instances(intersect, entry) unless intersect.empty?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sets instance variables in the current object based on
|
30
|
+
# values found in the XML document and the mapping specified
|
31
|
+
# in GoogleApps::Atom::MAPS
|
32
|
+
|
33
|
+
# @param [Array] intersect
|
34
|
+
# @param [LibXML::XML::Node] node
|
35
|
+
# @param [Hash] map
|
36
|
+
#
|
37
|
+
# @visibility public
|
38
|
+
# @return
|
39
|
+
def set_instances(intersect, node)
|
40
|
+
intersect.each do |attribute|
|
41
|
+
instance_variable_set "@#{@map[attribute]}", check_value(node.attributes[attribute])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module GoogleAppsOauth2
|
2
|
+
module Atom
|
3
|
+
class Feed < Document
|
4
|
+
attr_reader :doc, :items, :next_page
|
5
|
+
def initialize(body)
|
6
|
+
super(body)
|
7
|
+
|
8
|
+
@items = entries_from(document: @doc, entry_tag: 'entry')
|
9
|
+
end
|
10
|
+
|
11
|
+
def entries_from(properties)
|
12
|
+
properties[:document].root.inject([]) do |results, entry|
|
13
|
+
if entry.name == properties[:entry_tag]
|
14
|
+
results << new_doc(node_to_ary(entry), ['apps:', 'atom:', 'gd:'])
|
15
|
+
end
|
16
|
+
set_next_page(entry) if entry.name == 'link' and entry.attributes[:rel] == 'next'
|
17
|
+
results
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_next_page(node)
|
22
|
+
@next_page = node.attributes[:href]
|
23
|
+
end
|
24
|
+
|
25
|
+
# node_to_ary converts a Atom::XML::Node to an array.
|
26
|
+
#
|
27
|
+
# node_to_ary node
|
28
|
+
#
|
29
|
+
# node_to_ary returns the string representation of the
|
30
|
+
# given node split on \n.
|
31
|
+
def node_to_ary(node)
|
32
|
+
node.to_s.split("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
# new_doc creates a new Atom document from the data
|
36
|
+
# provided in the feed. new_doc takes a type, an
|
37
|
+
# array of content to be placed into the document
|
38
|
+
# as well as an array of filters.
|
39
|
+
#
|
40
|
+
# new_doc 'user', content_array, ['apps:']
|
41
|
+
#
|
42
|
+
# new_doc returns an GoogleApps::Atom document of the
|
43
|
+
# specified type.
|
44
|
+
def new_doc(content_array, filters)
|
45
|
+
content_array = filters.map do |filter|
|
46
|
+
grab_elements(content_array, filter)
|
47
|
+
end
|
48
|
+
|
49
|
+
add_category(content_array)
|
50
|
+
|
51
|
+
Atom.send :user, entry_wrap(content_array.flatten).join("\n")
|
52
|
+
end
|
53
|
+
|
54
|
+
# add_category adds the proper atom:category node to the
|
55
|
+
# content_array
|
56
|
+
#
|
57
|
+
# add_category content_array, 'user'
|
58
|
+
#
|
59
|
+
# add_category returns the modified content_array
|
60
|
+
def add_category(content_array)
|
61
|
+
content_array.unshift(create_node(type: 'atom:category', attrs: Atom::CATEGORY[:user]).to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
# grab_elements applies the specified filter to the
|
65
|
+
# provided array. Google's feed provides a lot of data
|
66
|
+
# that we don't need in an entry document.
|
67
|
+
#
|
68
|
+
# grab_elements content_array, 'apps:'
|
69
|
+
#
|
70
|
+
# grab_elements returns an array of items from content_array
|
71
|
+
# that match the given filter.
|
72
|
+
def grab_elements(content_array, filter)
|
73
|
+
content_array.grep(Regexp.new filter)
|
74
|
+
end
|
75
|
+
|
76
|
+
# entry_wrap adds atom:entry opening and closing tags
|
77
|
+
# to the provided content_array and the beginning and
|
78
|
+
# end.
|
79
|
+
#
|
80
|
+
# entry_wrap content_array
|
81
|
+
#
|
82
|
+
# entry_wrap returns an array with an opening atom:entry
|
83
|
+
# element prepended to the front and a closing atom:entry
|
84
|
+
# tag appended to the end.
|
85
|
+
def entry_wrap(content_array)
|
86
|
+
content_array.unshift(Atom::ENTRY_TAG[0]).push(Atom::ENTRY_TAG[1])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module GoogleAppsOauth2
|
2
|
+
module Atom
|
3
|
+
module Node
|
4
|
+
|
5
|
+
# create_node takes a hash of properties from which to
|
6
|
+
# build the XML node. The properties hash must have
|
7
|
+
# a :type key, it is also possible to pass an :attrs
|
8
|
+
# key with an array of attribute name, value pairs.
|
9
|
+
#
|
10
|
+
# create_node type: 'apps:property', attrs: [['name', 'Tim'], ['userName', 'tim@bob.com']]
|
11
|
+
#
|
12
|
+
# create_node returns an Atom::XML::Node with the specified
|
13
|
+
# properties.
|
14
|
+
def create_node(properties)
|
15
|
+
if properties[:attrs]
|
16
|
+
add_attributes Atom::XML::Node.new(properties[:type]), properties[:attrs]
|
17
|
+
else
|
18
|
+
Atom::XML::Node.new properties[:type]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# add_attributes adds the specified attributes to the
|
24
|
+
# given node. It takes a LibXML::XML::Node and an
|
25
|
+
# array of name, value attribute pairs.
|
26
|
+
#
|
27
|
+
# add_attribute node, [['title', 'emperor'], ['name', 'Napoleon']]
|
28
|
+
#
|
29
|
+
# add_attribute returns the modified node.
|
30
|
+
def add_attributes(node, attributes)
|
31
|
+
attributes.each do |attribute|
|
32
|
+
node.attributes[attribute[0]] = attribute[1]
|
33
|
+
end
|
34
|
+
|
35
|
+
node
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns true if "true" and false if "false"
|
39
|
+
|
40
|
+
#
|
41
|
+
# @param [String] value
|
42
|
+
#
|
43
|
+
# @visibility public
|
44
|
+
# @return
|
45
|
+
def check_value(value)
|
46
|
+
case value
|
47
|
+
when 'true'
|
48
|
+
true
|
49
|
+
when 'false'
|
50
|
+
false
|
51
|
+
else
|
52
|
+
value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module GoogleAppsOauth2
|
2
|
+
module Atom
|
3
|
+
class User < Document
|
4
|
+
attr_reader :doc, :login, :suspended, :first_name, :last_name, :quota, :password
|
5
|
+
|
6
|
+
MAP = {
|
7
|
+
userName: :login,
|
8
|
+
suspended: :suspended,
|
9
|
+
familyName: :last_name,
|
10
|
+
givenName: :first_name,
|
11
|
+
limit: :quota,
|
12
|
+
password: :password
|
13
|
+
}
|
14
|
+
|
15
|
+
def initialize(xml)
|
16
|
+
super(xml, MAP)
|
17
|
+
find_values
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module GoogleAppsOauth2
|
4
|
+
module Parsers
|
5
|
+
class FeedParser < HTTParty::Parser
|
6
|
+
SupportedFormats = {"application/atom+xml" => :atom}
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def atom
|
11
|
+
GoogleAppsOauth2::Atom::Feed.new(body).items
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'openssl'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'httparty'
|
5
|
+
|
6
|
+
module GoogleAppsOauth2
|
7
|
+
class Transport
|
8
|
+
include HTTParty
|
9
|
+
|
10
|
+
parser GoogleAppsOauth2::Parsers::FeedParser
|
11
|
+
|
12
|
+
base_uri 'https://apps-apis.google.com/a/feeds'
|
13
|
+
|
14
|
+
def initialize(options, &block)
|
15
|
+
@domain = options[:domain]
|
16
|
+
@token = options[:token]
|
17
|
+
@client_id = options[:client_id]
|
18
|
+
@client_secret = options[:client_secret]
|
19
|
+
@refresh_token = options[:refresh_token]
|
20
|
+
@token_changed_block = block
|
21
|
+
|
22
|
+
@user_path= "/#{@domain}/user/2.0"
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_users(options = {})
|
26
|
+
headers = {'content-type' => 'application/atom+xml', 'Authorization' => "OAuth #{token}"}
|
27
|
+
|
28
|
+
url = user_path + "?startUsername=#{options[:start]}"
|
29
|
+
response = self.class.get(url, headers: headers)
|
30
|
+
response = check_for_refresh(response)
|
31
|
+
|
32
|
+
response
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
attr_reader :user_path
|
37
|
+
attr_reader :domain, :token, :refresh_token, :client_id, :client_secret
|
38
|
+
|
39
|
+
def check_for_refresh(old_response)
|
40
|
+
response = old_response
|
41
|
+
if old_response.code == 401
|
42
|
+
data = {
|
43
|
+
:client_id => @client_id,
|
44
|
+
:client_secret => @client_secret,
|
45
|
+
:refresh_token => refresh_token,
|
46
|
+
:grant_type => "refresh_token"
|
47
|
+
}
|
48
|
+
response_json = MultiJson.load(self.class.post("https://accounts.google.com/o/oauth2/token", :body => data))
|
49
|
+
@token = response_json["access_token"]
|
50
|
+
@token_changed_block.call(@token)
|
51
|
+
headers = old_response.request.options[:headers].merge("Authorization" => "OAuth #{token}")
|
52
|
+
response = self.class.get(old_response.request.uri.to_s, headers: headers)
|
53
|
+
end
|
54
|
+
response
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
{
|
2
|
+
"access_token": "some-new-token",
|
3
|
+
"token_type": "Bearer",
|
4
|
+
"expires_in": 3600,
|
5
|
+
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjNmNDUwOTU1YzA3ZGUyZWFhNDA1OTg1NmMzOGFmYWY1OTFmMWUxYTcifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiY2lkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJsb29wYi5hYyIsInRva2VuX2hhc2giOiI3RnV3YXBnV1dXaWxHc1l6dm9SZURBIiwiYXRfaGFzaCI6IjdGdXdhcGdXV1dpbEdzWXp2b1JlREEiLCJ2ZXJpZmllZF9lbWFpbCI6InRydWUiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6IndpbGxAbG9vcGIuYWMiLCJpZCI6IjEwMjY2MDg3NTAyMjk1NTU4OTU2MCIsInN1YiI6IjEwMjY2MDg3NTAyMjk1NTU4OTU2MCIsImF1ZCI6IjQwNzQwODcxODE5Mi5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImlhdCI6MTM2MTQ5ODU5OSwiZXhwIjoxMzYxNTAyNDk5fQ.HblnwmqqyQoapWIZP90QqB92MBTUQEnYRjvFRO8BaWV2VVQIgc2v1Vb_oOaChNs3w2vXk-2NQiJlWVdqDFARCRe5n3BID2wr8iwSUzVCEt4fg3rqxQ-IeXXWNYCx-32N3k7w4mcnxsnglw7ajol4vkv-2Nx8peA3Zvwt0NXdxj8"
|
6
|
+
}
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:apps="http://schemas.google.com/apps/2006">
|
2
|
+
<atom:category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/apps/2006#user"/>
|
3
|
+
<apps:login userName="lholcomb2" suspended="false"/>
|
4
|
+
<apps:name familyName="Holcomb" givenName="Lawrence"/>
|
5
|
+
</atom:entry>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
2
|
+
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
|
3
|
+
xmlns:apps='http://schemas.google.com/apps/2006' xmlns:gd='http://schemas.google.com/g/2005'>
|
4
|
+
<id>https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0</id>
|
5
|
+
<updated>1970-01-01T00:00:00.000Z</updated>
|
6
|
+
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#user'/>
|
7
|
+
<title type='text'>Users</title>
|
8
|
+
<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml'
|
9
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0'/>
|
10
|
+
<link rel='http://schemas.google.com/g/2005#post' type='application/atom+xml'
|
11
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0'/>
|
12
|
+
<link rel='self' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0'/>
|
13
|
+
<link rel='next' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/cnm.edu/user/2.0?startUsername=aadams37'/>
|
14
|
+
<openSearch:startIndex>1</openSearch:startIndex>
|
15
|
+
<entry>
|
16
|
+
<id>https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/misterunfriendly</id>
|
17
|
+
<updated>1970-01-01T00:00:00.000Z</updated>
|
18
|
+
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#user'/>
|
19
|
+
<title type='text'>misterunfriendly</title>
|
20
|
+
<link rel='self' type='application/atom+xml'
|
21
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/misterunfriendly'/>
|
22
|
+
<link rel='edit' type='application/atom+xml'
|
23
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/misterunfriendly'/>
|
24
|
+
<apps:login userName='misterunfriendly' suspended='false' ipWhitelisted='false' admin='false'
|
25
|
+
changePasswordAtNextLogin='true' agreedToTerms='true'/>
|
26
|
+
<apps:quota limit='25600'/>
|
27
|
+
<apps:name familyName='Unfriendly' givenName='Mister'/>
|
28
|
+
<gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
|
29
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/nickname/2.0?username=misterunfriendly'/>
|
30
|
+
<gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
|
31
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/emailList/2.0?recipient=misterunfriendly%40loopb.ac'/>
|
32
|
+
</entry>
|
33
|
+
<entry>
|
34
|
+
<id>https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/will</id>
|
35
|
+
<updated>1970-01-01T00:00:00.000Z</updated>
|
36
|
+
<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/apps/2006#user'/>
|
37
|
+
<title type='text'>will</title>
|
38
|
+
<link rel='self' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/will'/>
|
39
|
+
<link rel='edit' type='application/atom+xml' href='https://apps-apis.google.com/a/feeds/loopb.ac/user/2.0/will'/>
|
40
|
+
<apps:login userName='will' suspended='false' ipWhitelisted='false' admin='true' changePasswordAtNextLogin='false'
|
41
|
+
agreedToTerms='true'/>
|
42
|
+
<apps:quota limit='25600'/>
|
43
|
+
<apps:name familyName='Read' givenName='Will'/>
|
44
|
+
<gd:feedLink rel='http://schemas.google.com/apps/2006#user.nicknames'
|
45
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/nickname/2.0?username=will'/>
|
46
|
+
<gd:feedLink rel='http://schemas.google.com/apps/2006#user.emailLists'
|
47
|
+
href='https://apps-apis.google.com/a/feeds/loopb.ac/emailList/2.0?recipient=will%40loopb.ac'/>
|
48
|
+
</entry>
|
49
|
+
</feed>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GoogleAppsOauth2::Atom::Document do
|
4
|
+
let (:document) { GoogleAppsOauth2::Atom::Document.new File.read('spec/fixture_xml/user.xml') }
|
5
|
+
let (:doc_string) { File.read('spec/fixture_xml/users_feed.xml') }
|
6
|
+
|
7
|
+
describe "#parse" do
|
8
|
+
it "parses the given XML document" do
|
9
|
+
document.parse(doc_string).should be_a LibXML::XML::Document
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#to_s" do
|
14
|
+
it "Outputs @doc as a string" do
|
15
|
+
document.to_s.should be_a String
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|