Merge lp:~thisfred/ubuntuone-couch/add-tests into lp:~thisfred/ubuntuone-couch/trunk
- add-tests
- Merge into trunk
Proposed by
Eric Casteleijn
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Eric Casteleijn | ||||||||
Approved revision: | 14 | ||||||||
Merged at revision: | 2 | ||||||||
Proposed branch: | lp:~thisfred/ubuntuone-couch/add-tests | ||||||||
Merge into: | lp:~thisfred/ubuntuone-couch/trunk | ||||||||
Diff against target: |
614 lines (+391/-181) 3 files modified
bin/u1couch-query (+32/-181) tests/test_u1couchquery.py (+164/-0) u1couch/query.py (+195/-0) |
||||||||
To merge this branch: | bzr merge lp:~thisfred/ubuntuone-couch/add-tests | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Alejandro J. Cura (community) | Approve | ||
Eric Casteleijn | Pending | ||
Review via email:
|
Commit message
Fixed and refactored u1couch-query and added tests
Description of the change
Fixed and refactored u1couch-query and added tests
To post a comment you must log in.
- 14. By Eric Casteleijn
-
unchanged: attach bug
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Natalia Bidart (nataliabidart) wrote : | # |
Added bug #720928 to use u1 specific login service.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === renamed file 'bin/ubuntuone-couchdb-query' => 'bin/u1couch-query' (properties changed: -x to +x) | |||
2 | --- bin/ubuntuone-couchdb-query 2011-02-14 20:21:08 +0000 | |||
3 | +++ bin/u1couch-query 2011-02-17 19:10:44 +0000 | |||
4 | @@ -1,204 +1,55 @@ | |||
5 | 1 | #!/usr/bin/python | 1 | #!/usr/bin/python |
6 | 2 | |||
7 | 3 | # Copyright 2011 Canonical Ltd. | ||
8 | 4 | # | ||
9 | 5 | # Ubuntu One Couch is free software: you can redistribute it and/or | ||
10 | 6 | # modify it under the terms of the GNU Lesser General Public License | ||
11 | 7 | # version 3 as published by the Free Software Foundation. | ||
12 | 8 | # | ||
13 | 9 | # Ubuntu One Couch is distributed in the hope that it will be useful, | ||
14 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
15 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
16 | 12 | # Lesser General Public License for more details. | ||
17 | 13 | # | ||
18 | 14 | # You should have received a copy of the GNU Lesser General Public | ||
19 | 15 | # License along with desktopcouch. If not, see | ||
20 | 16 | # <http://www.gnu.org/licenses/>. | ||
21 | 17 | |||
22 | 2 | """ubuntuone-couchdb-query: Command line query tool for Ubuntu One CouchDBs""" | 18 | """ubuntuone-couchdb-query: Command line query tool for Ubuntu One CouchDBs""" |
23 | 3 | 19 | ||
24 | 4 | # sil@kryogenix.org, 2010-02-13 | ||
25 | 5 | 20 | ||
26 | 6 | from optparse import OptionParser | 21 | from optparse import OptionParser |
27 | 7 | from oauth import oauth | ||
28 | 8 | import gnomekeyring, gobject, httplib2, simplejson, urlparse, cgi, urllib | ||
29 | 9 | |||
30 | 10 | try: | ||
31 | 11 | from ubuntu_sso.main import SSOCredentials | ||
32 | 12 | except ImportError: | ||
33 | 13 | SSOCredentials = None | ||
34 | 14 | 22 | ||
35 | 15 | import socket | 23 | import socket |
36 | 24 | from u1couch import query | ||
37 | 25 | |||
38 | 16 | socket.setdefaulttimeout(5) | 26 | socket.setdefaulttimeout(5) |
39 | 17 | 27 | ||
40 | 18 | def get_prod_oauth_token(): | ||
41 | 19 | """Get the token from the keyring""" | ||
42 | 20 | gobject.set_application_name("Ubuntu One Web API Tool") | ||
43 | 21 | |||
44 | 22 | consumer = oauth.OAuthConsumer("ubuntuone", "hammertime") | ||
45 | 23 | items = [] | ||
46 | 24 | items = gnomekeyring.find_items_sync( | ||
47 | 25 | gnomekeyring.ITEM_GENERIC_SECRET, | ||
48 | 26 | {'ubuntuone-realm': "https://ubuntuone.com", | ||
49 | 27 | 'oauth-consumer-key': consumer.key}) | ||
50 | 28 | return oauth.OAuthToken.from_string(items[0].secret) | ||
51 | 29 | |||
52 | 30 | def get_prod_oauth_token(explicit_token_store=None): | ||
53 | 31 | """Get the token from the keyring""" | ||
54 | 32 | gobject.set_application_name("Ubuntu One Web API Tool") | ||
55 | 33 | if (SSOCredentials is not None) and (explicit_token_store in ["sso", None]): | ||
56 | 34 | creds = SSOCredentials('Ubuntu One') | ||
57 | 35 | info = creds.find_credentials('Ubuntu One') | ||
58 | 36 | consumer = oauth.OAuthConsumer(info['consumer_key'], | ||
59 | 37 | info['consumer_secret']) | ||
60 | 38 | secret='oauth_token=%s&oauth_token_secret=%s' % (info['token'], | ||
61 | 39 | info['token_secret']) | ||
62 | 40 | elif explicit_token_store in ["hammertime", None]: | ||
63 | 41 | consumer = oauth.OAuthConsumer("ubuntuone", "hammertime") | ||
64 | 42 | items = [] | ||
65 | 43 | items = gnomekeyring.find_items_sync( | ||
66 | 44 | gnomekeyring.ITEM_GENERIC_SECRET, | ||
67 | 45 | {'ubuntuone-realm': "https://ubuntuone.com", | ||
68 | 46 | 'oauth-consumer-key': consumer.key}) | ||
69 | 47 | secret = items[0].secret | ||
70 | 48 | else: | ||
71 | 49 | raise Exception("Wasn't able to get a token") | ||
72 | 50 | |||
73 | 51 | return (oauth.OAuthToken.from_string(secret), consumer) | ||
74 | 52 | |||
75 | 53 | def get_oauth_request_header(consumer, access_token, http_url, signature_method): | ||
76 | 54 | """Get an oauth request header given the token and the url""" | ||
77 | 55 | assert http_url.startswith("https") | ||
78 | 56 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( | ||
79 | 57 | http_url=http_url, | ||
80 | 58 | http_method="GET", | ||
81 | 59 | oauth_consumer=consumer, | ||
82 | 60 | token=access_token) | ||
83 | 61 | oauth_request.sign_request(signature_method, consumer, access_token) | ||
84 | 62 | return oauth_request.to_header() | ||
85 | 63 | |||
86 | 64 | def request(urlpath, sigmeth, http_method, request_body, show_tokens, | ||
87 | 65 | server_override=None, explicit_token_store=None): | ||
88 | 66 | """Make a request to couchdb.one.ubuntu.com for the user's data. | ||
89 | 67 | |||
90 | 68 | The user supplies a urlpath (for example, dbname). We need to actually | ||
91 | 69 | request https://couchdb.one.ubuntu.com/PREFIX/dbname, and sign it with | ||
92 | 70 | the user's OAuth token, which must be in the keyring. | ||
93 | 71 | |||
94 | 72 | We find the prefix by querying https://one.ubuntu.com/api/account/ | ||
95 | 73 | (see desktopcouch.replication_services.ubuntuone, which does this). | ||
96 | 74 | """ | ||
97 | 75 | |||
98 | 76 | # First check that there's a token in the keyring. | ||
99 | 77 | try: | ||
100 | 78 | (access_token, consumer) = get_prod_oauth_token(explicit_token_store) | ||
101 | 79 | except: # bare except, norty | ||
102 | 80 | raise Exception("Unable to retrieve your Ubuntu One access details " | ||
103 | 81 | "from the keyring. Try connecting your machine to Ubuntu One.") | ||
104 | 82 | |||
105 | 83 | # Set the signature method. This should be HMAC unless you have a jolly | ||
106 | 84 | # good reason for it to not be. | ||
107 | 85 | if sigmeth == "PLAINTEXT": | ||
108 | 86 | signature_method = oauth.OAuthSignatureMethod_PLAINTEXT() | ||
109 | 87 | elif sigmeth == "HMAC_SHA1": | ||
110 | 88 | signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() | ||
111 | 89 | else: | ||
112 | 90 | signature_method = oauth.OAuthSignatureMethod_PLAINTEXT() | ||
113 | 91 | |||
114 | 92 | if show_tokens: | ||
115 | 93 | print "Using OAuth data:" | ||
116 | 94 | print "consumer: %s : %s\ntoken: %s" % ( | ||
117 | 95 | consumer.key, consumer.secret, access_token) | ||
118 | 96 | |||
119 | 97 | # Look up the user's prefix | ||
120 | 98 | infourl = "https://one.ubuntu.com/api/account/" | ||
121 | 99 | oauth_header = get_oauth_request_header(consumer, access_token, infourl, signature_method) | ||
122 | 100 | client = httplib2.Http() | ||
123 | 101 | resp, content = client.request(infourl, "GET", headers=oauth_header) | ||
124 | 102 | if resp['status'] == "200": | ||
125 | 103 | try: | ||
126 | 104 | document = simplejson.loads(content) | ||
127 | 105 | except ValueError: | ||
128 | 106 | raise Exception("Got unexpected content:\n%s" % content) | ||
129 | 107 | if "couchdb_root" not in document: | ||
130 | 108 | raise ValueError("couchdb_root not found in %s" % (document,)) | ||
131 | 109 | if "id" not in document: | ||
132 | 110 | raise ValueError("id not found in %s" % (document,)) | ||
133 | 111 | COUCH_ROOT = document["couchdb_root"] | ||
134 | 112 | USERID = document["id"] | ||
135 | 113 | else: | ||
136 | 114 | raise ValueError("Error retrieving user data (%s)" % (resp['status'], | ||
137 | 115 | url)) | ||
138 | 116 | |||
139 | 117 | # COUCH_ROOT must have all internal slashes escaped | ||
140 | 118 | schema, netloc, path, params, query, fragment = urlparse.urlparse(COUCH_ROOT) | ||
141 | 119 | if server_override: | ||
142 | 120 | netloc = server_override | ||
143 | 121 | path = "/" + urllib.quote(path[1:], safe="") # don't escape the first / | ||
144 | 122 | COUCH_ROOT = urlparse.urlunparse((schema, netloc, path, params, query, fragment)) | ||
145 | 123 | |||
146 | 124 | # Now use COUCH_ROOT and the specified user urlpath to get data | ||
147 | 125 | if urlpath == "_all_dbs": | ||
148 | 126 | couch_url = "%s%s" % (COUCH_ROOT, urlpath) | ||
149 | 127 | couch_url = urlparse.urlunparse((schema, netloc, "_all_dbs", None, "user_id=%s" % USERID, None)) | ||
150 | 128 | else: | ||
151 | 129 | couch_url = "%s%%2F%s" % (COUCH_ROOT, urlpath) | ||
152 | 130 | schema, netloc, path, params, query, fragment = urlparse.urlparse(couch_url) | ||
153 | 131 | querystr_as_dict = dict(cgi.parse_qsl(query)) | ||
154 | 132 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( | ||
155 | 133 | http_url=couch_url, | ||
156 | 134 | http_method=http_method, | ||
157 | 135 | oauth_consumer=consumer, | ||
158 | 136 | token=access_token, | ||
159 | 137 | parameters=querystr_as_dict) | ||
160 | 138 | oauth_request.sign_request(signature_method, consumer, access_token) | ||
161 | 139 | failed = 0 | ||
162 | 140 | #print "Connecting to effective Couch URL", oauth_request.to_url() | ||
163 | 141 | #print "Connecting to actual Couch URL (with OAuth header)", couch_url | ||
164 | 142 | #print oauth_request.to_header(), querystr_as_dict | ||
165 | 143 | while 1: | ||
166 | 144 | try: | ||
167 | 145 | resp, content = client.request(couch_url, http_method, | ||
168 | 146 | headers=oauth_request.to_header(), body=request_body) | ||
169 | 147 | break | ||
170 | 148 | except IOError: | ||
171 | 149 | failed += 1 | ||
172 | 150 | if failed > 0: | ||
173 | 151 | if failed == 1: | ||
174 | 152 | s = "" | ||
175 | 153 | else: | ||
176 | 154 | s = "s" | ||
177 | 155 | print "(request failed %s time%s)" % (failed, s) | ||
178 | 156 | if resp['status'] == "200": | ||
179 | 157 | try: | ||
180 | 158 | return simplejson.loads(content) | ||
181 | 159 | except: | ||
182 | 160 | print "(data returned from CouchDB was invalid JSON)" | ||
183 | 161 | return content | ||
184 | 162 | elif resp['status'] == "400": | ||
185 | 163 | print "The server could not parse the oauth token:\n%s" % content | ||
186 | 164 | elif resp['status'] == "401": | ||
187 | 165 | print "Access Denied" | ||
188 | 166 | print "Content:" | ||
189 | 167 | return content | ||
190 | 168 | else: | ||
191 | 169 | return ( | ||
192 | 170 | "There was a problem processing the request:\nstatus:%s, response:" | ||
193 | 171 | " %r" % (resp['status'], content)) | ||
194 | 172 | |||
195 | 173 | 28 | ||
196 | 174 | if __name__ == "__main__": | 29 | if __name__ == "__main__": |
199 | 175 | parser = OptionParser(usage="prog [options] urlpath") | 30 | PARSER = OptionParser(usage="prog [options] urlpath") |
200 | 176 | parser.add_option("--oauth-signature-method", dest="sigmeth", | 31 | PARSER.add_option("--oauth-signature-method", dest="sigmeth", |
201 | 177 | default="HMAC_SHA1", | 32 | default="HMAC_SHA1", |
202 | 178 | help="OAuth signature method to use (PLAINTEXT or " | 33 | help="OAuth signature method to use (PLAINTEXT or " |
203 | 179 | "HMAC_SHA1)") | 34 | "HMAC_SHA1)") |
205 | 180 | parser.add_option("--http-method", dest="http_method", | 35 | PARSER.add_option("--http-method", dest="http_method", |
206 | 181 | default="GET", | 36 | default="GET", |
207 | 182 | help="HTTP method to use") | 37 | help="HTTP method to use") |
209 | 183 | parser.add_option("--body", dest="body", | 38 | PARSER.add_option("--body", dest="body", |
210 | 184 | default=None, | 39 | default=None, |
211 | 185 | help="HTTP request body") | 40 | help="HTTP request body") |
213 | 186 | parser.add_option("--show-tokens", dest="show_tokens", | 41 | PARSER.add_option("--show-tokens", dest="show_tokens", |
214 | 187 | default=False, | 42 | default=False, |
215 | 188 | help="Show the OAuth tokens we're using") | 43 | help="Show the OAuth tokens we're using") |
217 | 189 | parser.add_option("--server-override", dest="server_override", | 44 | PARSER.add_option("--server-override", dest="server_override", |
218 | 190 | default=None, | 45 | default=None, |
219 | 191 | help="Use a different server") | 46 | help="Use a different server") |
220 | 192 | parser.add_option("--explicit-token-store", dest="explicit_token_store", | ||
221 | 193 | default=None, | ||
222 | 194 | help="Explicitly choose a token store (sso or hammertime)") | ||
223 | 195 | 47 | ||
233 | 196 | (options, args) = parser.parse_args() | 48 | (OPTIONS, ARGS) = PARSER.parse_args() |
234 | 197 | if len(args) != 1: | 49 | if len(ARGS) != 1: |
235 | 198 | parser.error("You must specify a urlpath (e.g., a dbname)") | 50 | PARSER.error("You must specify a urlpath (e.g., a dbname)") |
236 | 199 | print request( | 51 | print query.request( |
237 | 200 | urlpath=args[0], sigmeth=options.sigmeth, | 52 | urlpath=ARGS[0], sig_meth=OPTIONS.sigmeth, |
238 | 201 | http_method=options.http_method, request_body=options.body, | 53 | http_method=OPTIONS.http_method, request_body=OPTIONS.body, |
239 | 202 | show_tokens=options.show_tokens, | 54 | show_tokens=OPTIONS.show_tokens, |
240 | 203 | server_override=options.server_override, | 55 | server_override=OPTIONS.server_override) |
232 | 204 | explicit_token_store=options.explicit_token_store) | ||
241 | 205 | 56 | ||
242 | === renamed file 'bin/ubuntuone-sign-uri' => 'bin/ubuntuone-sign-uri.py' (properties changed: -x to +x) | |||
243 | === added directory 'tests' | |||
244 | === added file 'tests/__init__.py' | |||
245 | === added file 'tests/test_u1couchquery.py' | |||
246 | --- tests/test_u1couchquery.py 1970-01-01 00:00:00 +0000 | |||
247 | +++ tests/test_u1couchquery.py 2011-02-17 19:10:44 +0000 | |||
248 | @@ -0,0 +1,164 @@ | |||
249 | 1 | # Copyright 2011 Canonical Ltd. | ||
250 | 2 | # | ||
251 | 3 | # Ubuntu One Couch is free software: you can redistribute it and/or | ||
252 | 4 | # modify it under the terms of the GNU Lesser General Public License | ||
253 | 5 | # version 3 as published by the Free Software Foundation. | ||
254 | 6 | # | ||
255 | 7 | # Ubuntu One Couch is distributed in the hope that it will be useful, | ||
256 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
257 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
258 | 10 | # Lesser General Public License for more details. | ||
259 | 11 | # | ||
260 | 12 | # You should have received a copy of the GNU Lesser General Public | ||
261 | 13 | # License along with desktopcouch. If not, see | ||
262 | 14 | # <http://www.gnu.org/licenses/>. | ||
263 | 15 | |||
264 | 16 | """Tests for u1couch.query.""" | ||
265 | 17 | |||
266 | 18 | from twisted.trial.unittest import TestCase | ||
267 | 19 | from mocker import Mocker, ANY | ||
268 | 20 | |||
269 | 21 | import ubuntu_sso | ||
270 | 22 | import json | ||
271 | 23 | from oauth import oauth | ||
272 | 24 | from u1couch import query | ||
273 | 25 | |||
274 | 26 | CONSUMER_KEY = u'this_consumer_key' | ||
275 | 27 | CONSUMER_SECRET = u'sssssh!' | ||
276 | 28 | TOKEN_KEY = u'tokentokentoken' | ||
277 | 29 | TOKEN_SECRET = u'ssssssshhhhhh!' | ||
278 | 30 | |||
279 | 31 | CONSUMER = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET) | ||
280 | 32 | TOKEN = oauth.OAuthToken(TOKEN_KEY, TOKEN_SECRET) | ||
281 | 33 | |||
282 | 34 | URL = 'https://example.com' | ||
283 | 35 | |||
284 | 36 | |||
285 | 37 | class QueryTestCase(TestCase): | ||
286 | 38 | """Test case for u1couch.query.""" | ||
287 | 39 | |||
288 | 40 | def setUp(self): | ||
289 | 41 | self.mocker = Mocker() | ||
290 | 42 | |||
291 | 43 | def tearDown(self): | ||
292 | 44 | self.mocker.restore() | ||
293 | 45 | self.mocker.verify() | ||
294 | 46 | |||
295 | 47 | def test_get_oauth_request_header(self): | ||
296 | 48 | """Test get_oauth_request_header returns correct headers.""" | ||
297 | 49 | fake_headers = {'Authorization': | ||
298 | 50 | 'OAuth realm="", oauth_nonce="39941541", ' | ||
299 | 51 | 'oauth_timestamp="1297958903", ' | ||
300 | 52 | 'oauth_consumer_key="this_consumer_key", ' | ||
301 | 53 | 'oauth_signature_method="HMAC-SHA1", oauth_version="1.0", ' | ||
302 | 54 | 'oauth_token="tokentokentoken", ' | ||
303 | 55 | 'oauth_signature="TNIfersCweWluuuJW%2FT%2FbW9IHD0%3D"'} | ||
304 | 56 | mock_oauth = self.mocker.replace("oauth.oauth") | ||
305 | 57 | mock_oauth.OAuthRequest # pylint: disable=W0104 | ||
306 | 58 | MockOAuthRequest = self.mocker.mock() # pylint: disable=C0103 | ||
307 | 59 | self.mocker.result(MockOAuthRequest) | ||
308 | 60 | MockOAuthRequest.from_consumer_and_token( | ||
309 | 61 | http_url=URL, http_method='GET', oauth_consumer=CONSUMER, | ||
310 | 62 | token=TOKEN) | ||
311 | 63 | oauth_request = self.mocker.mock() | ||
312 | 64 | self.mocker.result(oauth_request) | ||
313 | 65 | oauth_request.sign_request(query.HMAC_SHA1, CONSUMER, TOKEN) | ||
314 | 66 | oauth_request.to_header() | ||
315 | 67 | self.mocker.result(fake_headers) | ||
316 | 68 | self.mocker.replay() | ||
317 | 69 | headers = query.get_oauth_request_header(CONSUMER, TOKEN, URL) | ||
318 | 70 | self.assertEquals(fake_headers, headers) | ||
319 | 71 | |||
320 | 72 | def test_get_oauth_request_header_http(self): | ||
321 | 73 | """Test get_oauth_request_header fails on http urls.""" | ||
322 | 74 | self.assertRaises( | ||
323 | 75 | AssertionError, query.get_oauth_request_header, CONSUMER, TOKEN, | ||
324 | 76 | 'http://example.com') | ||
325 | 77 | |||
326 | 78 | def test_get_oauth_data(self): | ||
327 | 79 | """Test get_oauth_data returns proper oauth data.""" | ||
328 | 80 | self.patch(query, "undbusify", lambda x: x) | ||
329 | 81 | dbus = self.mocker.replace("dbus") | ||
330 | 82 | self.mocker.replace("dbus.mainloop.glib.DBusGMainLoop") | ||
331 | 83 | bus = dbus.SessionBus() | ||
332 | 84 | bus.get_object( | ||
333 | 85 | ubuntu_sso.DBUS_BUS_NAME, ubuntu_sso.DBUS_CRED_PATH, | ||
334 | 86 | follow_name_owner_changes=True) | ||
335 | 87 | mock_proxy = self.mocker.mock() | ||
336 | 88 | self.mocker.result(mock_proxy) | ||
337 | 89 | mock_proxy.find_credentials(query.APP_NAME) | ||
338 | 90 | self.mocker.result({ | ||
339 | 91 | u'token': TOKEN_KEY, | ||
340 | 92 | u'token_secret': TOKEN_SECRET, | ||
341 | 93 | u'consumer_secret': CONSUMER_SECRET, | ||
342 | 94 | u'consumer_key': CONSUMER_KEY}) | ||
343 | 95 | self.mocker.replay() | ||
344 | 96 | oauth_data = query.get_oauth_data() | ||
345 | 97 | self.assertEquals(CONSUMER_KEY, oauth_data['consumer_key']) | ||
346 | 98 | self.assertEquals(CONSUMER_SECRET, oauth_data['consumer_secret']) | ||
347 | 99 | self.assertEquals(TOKEN_KEY, oauth_data['token']) | ||
348 | 100 | self.assertEquals(TOKEN_SECRET, oauth_data['token_secret']) | ||
349 | 101 | |||
350 | 102 | def test_get_prod_oauth_token(self): | ||
351 | 103 | """Test get_prod_oauth_token returns token and consumer.""" | ||
352 | 104 | info = { | ||
353 | 105 | u'token': TOKEN_KEY, | ||
354 | 106 | u'token_secret': TOKEN_SECRET, | ||
355 | 107 | u'consumer_secret': CONSUMER_SECRET, | ||
356 | 108 | u'consumer_key': CONSUMER_KEY} | ||
357 | 109 | token, consumer = query.get_prod_oauth_token(info) | ||
358 | 110 | self.assertEquals(TOKEN_KEY, token.key) | ||
359 | 111 | self.assertEquals(TOKEN_SECRET, token.secret) | ||
360 | 112 | self.assertEquals(CONSUMER_KEY, consumer.key) | ||
361 | 113 | self.assertEquals(CONSUMER_SECRET, consumer.secret) | ||
362 | 114 | |||
363 | 115 | def test_get_user_info(self): | ||
364 | 116 | """Test get_user_info parses json correctly.""" | ||
365 | 117 | mock_header = self.mocker.mock() | ||
366 | 118 | Http = self.mocker.replace("httplib2.Http") # pylint: disable=C0103 | ||
367 | 119 | http = Http() | ||
368 | 120 | http.request(URL, "GET", headers=mock_header) | ||
369 | 121 | self.mocker.result(( | ||
370 | 122 | {'status': '200'}, | ||
371 | 123 | '{"couchdb_root": "https://couchdb.one.ubuntu.com/u/abc/def/1337",' | ||
372 | 124 | ' "id": 1337}')) | ||
373 | 125 | self.mocker.replay() | ||
374 | 126 | user_id, root = query.get_user_info(URL, mock_header) | ||
375 | 127 | self.assertEquals(1337, user_id) | ||
376 | 128 | self.assertEquals( | ||
377 | 129 | "https://couchdb.one.ubuntu.com/u/abc/def/1337", root) | ||
378 | 130 | |||
379 | 131 | def test_request(self): | ||
380 | 132 | """Test a full request.""" | ||
381 | 133 | fake_result = { | ||
382 | 134 | 'committed_update_seq': 654, | ||
383 | 135 | 'compact_running': False, | ||
384 | 136 | 'db_name': 'u/abc/def/1337/contacts', | ||
385 | 137 | 'disk_format_version': 5, | ||
386 | 138 | 'disk_size': 929892, | ||
387 | 139 | 'doc_count': 626, | ||
388 | 140 | 'doc_del_count': 1, | ||
389 | 141 | 'instance_start_time': '1297965776474824', | ||
390 | 142 | 'purge_seq': 0, | ||
391 | 143 | 'update_seq': 654} | ||
392 | 144 | fake_json = json.dumps(fake_result) | ||
393 | 145 | get_oauth_data = self.mocker.replace('u1couch.query.get_oauth_data') | ||
394 | 146 | get_oauth_data() | ||
395 | 147 | self.mocker.result({ | ||
396 | 148 | u'token': TOKEN_KEY, | ||
397 | 149 | u'token_secret': TOKEN_SECRET, | ||
398 | 150 | u'consumer_secret': CONSUMER_SECRET, | ||
399 | 151 | u'consumer_key': CONSUMER_KEY}) | ||
400 | 152 | get_user_info = self.mocker.replace('u1couch.query.get_user_info') | ||
401 | 153 | get_user_info('https://one.ubuntu.com/api/account/', ANY) | ||
402 | 154 | self.mocker.result(( | ||
403 | 155 | 1337, "https://couchdb.one.ubuntu.com/u/abc/def/1337")) | ||
404 | 156 | Http = self.mocker.replace("httplib2.Http") # pylint: disable=C0103 | ||
405 | 157 | http = Http() | ||
406 | 158 | http.request( | ||
407 | 159 | 'https://couchdb.one.ubuntu.com/u%2Fabc%2Fdef%2F1337%2Fcontacts', | ||
408 | 160 | 'GET', headers=ANY, body=None) | ||
409 | 161 | self.mocker.result(({'status': '200'}, fake_json)) | ||
410 | 162 | self.mocker.replay() | ||
411 | 163 | result = query.request('contacts') | ||
412 | 164 | self.assertEquals(fake_result, result) | ||
413 | 0 | 165 | ||
414 | === added directory 'u1couch' | |||
415 | === added file 'u1couch/__init__.py' | |||
416 | === added file 'u1couch/query.py' | |||
417 | --- u1couch/query.py 1970-01-01 00:00:00 +0000 | |||
418 | +++ u1couch/query.py 2011-02-17 19:10:44 +0000 | |||
419 | @@ -0,0 +1,195 @@ | |||
420 | 1 | # Copyright 2011 Canonical Ltd. | ||
421 | 2 | # | ||
422 | 3 | # Ubuntu One Couch is free software: you can redistribute it and/or | ||
423 | 4 | # modify it under the terms of the GNU Lesser General Public License | ||
424 | 5 | # version 3 as published by the Free Software Foundation. | ||
425 | 6 | # | ||
426 | 7 | # Ubuntu One Couch is distributed in the hope that it will be useful, | ||
427 | 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
428 | 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
429 | 10 | # Lesser General Public License for more details. | ||
430 | 11 | # | ||
431 | 12 | # You should have received a copy of the GNU Lesser General Public | ||
432 | 13 | # License along with desktopcouch. If not, see | ||
433 | 14 | # <http://www.gnu.org/licenses/>. | ||
434 | 15 | |||
435 | 16 | """Small library for talking to Ubuntu One CouchDBs.""" | ||
436 | 17 | |||
437 | 18 | import cgi | ||
438 | 19 | import dbus | ||
439 | 20 | from dbus.mainloop.glib import DBusGMainLoop | ||
440 | 21 | import logging | ||
441 | 22 | import httplib2 | ||
442 | 23 | import json | ||
443 | 24 | import urllib | ||
444 | 25 | import urlparse | ||
445 | 26 | from time import sleep | ||
446 | 27 | from oauth import oauth | ||
447 | 28 | |||
448 | 29 | import ubuntu_sso | ||
449 | 30 | |||
450 | 31 | APP_NAME = "Ubuntu One" | ||
451 | 32 | HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1() | ||
452 | 33 | |||
453 | 34 | |||
454 | 35 | def undbusify(value): | ||
455 | 36 | """Convert dbus types back to native types.""" | ||
456 | 37 | for singleton in (None, True, False): | ||
457 | 38 | if value == singleton: | ||
458 | 39 | return singleton | ||
459 | 40 | for val_type in (long, int, float, complex, | ||
460 | 41 | unicode, str, | ||
461 | 42 | list, tuple, dict, set): | ||
462 | 43 | if isinstance(value, val_type): | ||
463 | 44 | return val_type(value) | ||
464 | 45 | raise TypeError(value) | ||
465 | 46 | |||
466 | 47 | |||
467 | 48 | def get_oauth_data(): | ||
468 | 49 | """Information needed to replicate to a server.""" | ||
469 | 50 | |||
470 | 51 | DBusGMainLoop(set_as_default=True) | ||
471 | 52 | |||
472 | 53 | bus = dbus.SessionBus() | ||
473 | 54 | proxy = bus.get_object( | ||
474 | 55 | ubuntu_sso.DBUS_BUS_NAME, ubuntu_sso.DBUS_CRED_PATH, | ||
475 | 56 | follow_name_owner_changes=True) | ||
476 | 57 | logging.info( | ||
477 | 58 | 'get_oauth_data: asking for credentials to Ubuntu SSO. App name: %s', | ||
478 | 59 | APP_NAME) | ||
479 | 60 | oauth_data = dict( | ||
480 | 61 | (undbusify(k), undbusify(v)) for k, v in | ||
481 | 62 | proxy.find_credentials(APP_NAME).iteritems()) | ||
482 | 63 | logging.info( | ||
483 | 64 | 'Got credentials from Ubuntu SSO. Non emtpy credentials? %s', | ||
484 | 65 | len(oauth_data) > 0) | ||
485 | 66 | return oauth_data | ||
486 | 67 | |||
487 | 68 | |||
488 | 69 | def get_prod_oauth_token(info): | ||
489 | 70 | """Get the token from the keyring.""" | ||
490 | 71 | consumer = oauth.OAuthConsumer( | ||
491 | 72 | info['consumer_key'], info['consumer_secret']) | ||
492 | 73 | secret = 'oauth_token=%s&oauth_token_secret=%s' % ( | ||
493 | 74 | info['token'], info['token_secret']) | ||
494 | 75 | |||
495 | 76 | return (oauth.OAuthToken.from_string(secret), consumer) | ||
496 | 77 | |||
497 | 78 | |||
498 | 79 | def get_oauth_request_header(consumer, access_token, http_url, | ||
499 | 80 | signature_method=HMAC_SHA1): | ||
500 | 81 | """Get an oauth request header given the token and the url.""" | ||
501 | 82 | assert http_url.startswith("https") | ||
502 | 83 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( | ||
503 | 84 | http_url=http_url, | ||
504 | 85 | http_method="GET", | ||
505 | 86 | oauth_consumer=consumer, | ||
506 | 87 | token=access_token) | ||
507 | 88 | oauth_request.sign_request(signature_method, consumer, access_token) | ||
508 | 89 | return oauth_request.to_header() | ||
509 | 90 | |||
510 | 91 | |||
511 | 92 | def get_user_info(info_url, oauth_header): | ||
512 | 93 | """Look up the user's user id and prefix.""" | ||
513 | 94 | http = httplib2.Http() | ||
514 | 95 | resp, content = http.request(info_url, "GET", headers=oauth_header) | ||
515 | 96 | if resp['status'] not in ("200", "201"): | ||
516 | 97 | raise ValueError( | ||
517 | 98 | "Error retrieving user data (%s, %s)" % resp['status']) | ||
518 | 99 | try: | ||
519 | 100 | document = json.loads(content) | ||
520 | 101 | except ValueError: | ||
521 | 102 | raise Exception("Got unexpected content:\n%s" % content) | ||
522 | 103 | if "couchdb_root" not in document: | ||
523 | 104 | raise ValueError("couchdb_root not found in %s" % (document,)) | ||
524 | 105 | if "id" not in document: | ||
525 | 106 | raise ValueError("id not found in %s" % (document,)) | ||
526 | 107 | return document["id"], document["couchdb_root"] | ||
527 | 108 | |||
528 | 109 | |||
529 | 110 | def request(urlpath, sig_meth='HMAC_SHA1', http_method='GET', | ||
530 | 111 | request_body=None, show_tokens=False, server_override=None, | ||
531 | 112 | access_token=None, consumer=None): | ||
532 | 113 | """Make a request to couchdb.one.ubuntu.com for the user's data. | ||
533 | 114 | |||
534 | 115 | The user supplies a urlpath (for example, dbname). We need to actually | ||
535 | 116 | request https://couchdb.one.ubuntu.com/PREFIX/dbname, and sign it with | ||
536 | 117 | the user's OAuth token. | ||
537 | 118 | |||
538 | 119 | We find the prefix by querying https://one.ubuntu.com/api/account/ | ||
539 | 120 | (see desktopcouch.replication_services.ubuntuone, which does this). | ||
540 | 121 | |||
541 | 122 | """ | ||
542 | 123 | if access_token is None: | ||
543 | 124 | info = get_oauth_data() | ||
544 | 125 | (access_token, consumer) = get_prod_oauth_token(info) | ||
545 | 126 | # Set the signature method. This should be HMAC unless you have a jolly | ||
546 | 127 | # good reason for it to not be. | ||
547 | 128 | if sig_meth == "PLAINTEXT": | ||
548 | 129 | signature_method = oauth.OAuthSignatureMethod_PLAINTEXT() | ||
549 | 130 | else: | ||
550 | 131 | signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() | ||
551 | 132 | if show_tokens: | ||
552 | 133 | print "Using OAuth data:" | ||
553 | 134 | print "consumer: %s : %s\ntoken: %s" % ( | ||
554 | 135 | consumer.key, consumer.secret, access_token) | ||
555 | 136 | |||
556 | 137 | info_url = "https://one.ubuntu.com/api/account/" | ||
557 | 138 | oauth_header = get_oauth_request_header( | ||
558 | 139 | consumer, access_token, info_url, signature_method) | ||
559 | 140 | |||
560 | 141 | userid, couch_root = get_user_info(info_url, oauth_header) | ||
561 | 142 | schema, netloc, path, params, query, fragment = urlparse.urlparse( | ||
562 | 143 | couch_root) | ||
563 | 144 | if server_override: | ||
564 | 145 | netloc = server_override | ||
565 | 146 | # Don't escape the first / | ||
566 | 147 | path = "/" + urllib.quote(path[1:], safe="") | ||
567 | 148 | couch_root = urlparse.urlunparse(( | ||
568 | 149 | schema, netloc, path, params, query, fragment)) | ||
569 | 150 | |||
570 | 151 | # Now use COUCH_ROOT and the specified user urlpath to get data | ||
571 | 152 | if urlpath == "_all_dbs": | ||
572 | 153 | couch_url = "%s%s" % (couch_root, urlpath) | ||
573 | 154 | couch_url = urlparse.urlunparse(( | ||
574 | 155 | schema, netloc, "_all_dbs", None, "user_id=%s" % userid, None)) | ||
575 | 156 | else: | ||
576 | 157 | couch_url = "%s%%2F%s" % (couch_root, urlpath) | ||
577 | 158 | schema, netloc, path, params, query, fragment = urlparse.urlparse( | ||
578 | 159 | couch_url) | ||
579 | 160 | querystr_as_dict = dict(cgi.parse_qsl(query)) | ||
580 | 161 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( | ||
581 | 162 | http_url=couch_url, | ||
582 | 163 | http_method=http_method, | ||
583 | 164 | oauth_consumer=consumer, | ||
584 | 165 | token=access_token, | ||
585 | 166 | parameters=querystr_as_dict) | ||
586 | 167 | oauth_request.sign_request(signature_method, consumer, access_token) | ||
587 | 168 | failed = 0 | ||
588 | 169 | while True: | ||
589 | 170 | try: | ||
590 | 171 | http = httplib2.Http() | ||
591 | 172 | print couch_url, http_method | ||
592 | 173 | resp, content = http.request(couch_url, http_method, | ||
593 | 174 | headers=oauth_request.to_header(), body=request_body) | ||
594 | 175 | break | ||
595 | 176 | except IOError: | ||
596 | 177 | sleep(2 ** failed) # 1, 2, 4, 8, 16 | ||
597 | 178 | failed += 1 | ||
598 | 179 | if failed > 5: | ||
599 | 180 | break | ||
600 | 181 | if failed: | ||
601 | 182 | print "(request failed %s time%s)" % ( | ||
602 | 183 | failed, "s" if failed > 1 else "") | ||
603 | 184 | if resp['status'] in ("200", "201"): | ||
604 | 185 | return json.loads(content) | ||
605 | 186 | elif resp['status'] == "400": | ||
606 | 187 | print "The server could not parse the oauth token:\n%s" % content | ||
607 | 188 | elif resp['status'] == "401": | ||
608 | 189 | print "Access Denied" | ||
609 | 190 | print "Content:" | ||
610 | 191 | return content | ||
611 | 192 | else: | ||
612 | 193 | return ( | ||
613 | 194 | "There was a problem processing the request:\nstatus:%s, response:" | ||
614 | 195 | " %r" % (resp['status'], content)) |
Great to see this being tested and tidied up. Good work!
I've added bug #720917 so the request method returns only json or Exceptions, and thisfred agreed to work on that on a subsequent branch.