Merge lp:~alecu/unity-lens-music/ubuntuone-webservices into lp:unity-lens-music
- ubuntuone-webservices
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Michal Hruby | ||||
Approved revision: | 122 | ||||
Merged at revision: | 122 | ||||
Proposed branch: | lp:~alecu/unity-lens-music/ubuntuone-webservices | ||||
Merge into: | lp:unity-lens-music | ||||
Diff against target: |
1247 lines (+1152/-3) 9 files modified
configure.ac (+3/-1) debian/changelog (+7/-0) debian/control (+2/-0) po/POTFILES.skip (+2/-0) src/Makefile.am (+5/-1) src/oauth.vapi (+19/-0) src/ubuntuone-webservices.vala (+342/-0) tests/unit/Makefile.am (+11/-1) tests/unit/test-ubuntuone-purchases.vala (+761/-0) |
||||
To merge this branch: | bzr merge lp:~alecu/unity-lens-music/ubuntuone-webservices | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michal Hruby (community) | Approve | ||
PS Jenkins bot | continuous-integration | Pending | |
Review via email:
|
Commit message
Call every webservice with libsoup, using OAuth when needed
Description of the change
This branch adds code to call the Ubuntu One webservices needed by dash payments, using libsoup and OAuth. It adds a few unit tests too.
- 116. By Alejandro J. Cura
-
merged with trunk
- 117. By Alejandro J. Cura
-
fixes requested in merge proposal
- 118. By Alejandro J. Cura
-
use purchase sku to get payment method
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Alejandro J. Cura (alecu) wrote : | # |
Thanks for your through review!
> 56 + public unowned string sign_url2(string url, string? postargs,
>
> Looking at the docs, it says that you need to free the return, so s/unowned//,
> plus the postargs should be "out string postargs" as it's also a return (out
> params are nullable by default).
Fixed as requested.
> 118 + internal Soup.SessionAsync http_session;
>
> "internal" is a special keyword used when building libraries in vala (internal
> symbols are callable by other parts of the library, but not by the consumers),
> pls use private/public as appropriate.
Fixed all unnecessary uses of "internal". A few remain, as needed in some tests.
> 177 + public string nickname {
> 178 + get { return _nickname; }
> 179 + }
> + other instances
>
> No need for the extra private variable, use "public string x { get; private
> set; }"
Fixed as requested.
> 243 + credentials_
> 244 + yield;
>
> Wouldn't it be better if find_credentials() itself was async? Then you
> wouldn't need the signals and you could just return the credentials / throw an
> error in the async method.
find_credentials is defined in lp:ubuntu-sso-client, and used via dbus. We found some timeout problems in the python dbus bindings that were solved by using signals instead, so most of the client code in Ubuntu SSO Client and Ubuntu One already uses this pattern. I agree that it would simplify things, but too much code depends on this dbus API as is, and it's not easy to change just for this case.
> 278 + queue_signed_
> (session, message) => {
>
> You're using this multiple times, perhaps add a helper async method that takes
> method + uri and returns flattened Soup.MessageBody?
As suggested, I created a helper async method for the repeated instances. There are still at least two places which did not follow the same pattern (one used different http object and error codes, the other was not async), so I kept the existing code for those.
> 285 + var result = (string) message.
>
> .data could be null, no? Or does the flatten() cause possible null to become
> ""?
I just checked, and if .data is null, flatten() does the right thing and turns it into an empty string.
> 576 + return 0;
>
> You should return result of Test.run ()
Fixed.
> 852 + MainLoop mainloop = new MainLoop ();
> 853 + purchase_
> 854 + mainloop.quit ();
> 855 + try {
> 856 + purchase_
> 857 + } catch (PurchaseError e) {
> 858 + error ("Can't fetch payment info: %s", e.message);
> 859 + }
> 860 + });
> 861 + mainloop.run ();
>
> I remember you mentioned on IRC that you weren't sure how to do async tests
> with vala, so yep, this is the way, usually we just have a wrapper for the
> mainloop.run() which also adds a timeout, so the test fails eventually if
> there's no response. Usuall...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michal Hruby (mhr3) wrote : | # |
Awesome, /me happy now :)
- 119. By Alejandro J. Cura
-
Link bug #1088935
- 120. By Alejandro J. Cura
-
Fixes requested for the packaging
- 121. By Alejandro J. Cura
-
merged with trunk
- 122. By Alejandro J. Cura
-
Packaging fixes requested on the review
Preview Diff
1 | === modified file 'configure.ac' |
2 | --- configure.ac 2012-11-14 14:45:52 +0000 |
3 | +++ configure.ac 2012-12-12 00:01:27 +0000 |
4 | @@ -63,7 +63,9 @@ |
5 | dee-1.0 >= 1.0.7 |
6 | sqlite3 >= 3.7.7 |
7 | gee-1.0 |
8 | - json-glib-1.0 |
9 | + json-glib-1.0 |
10 | + libsoup-2.4 |
11 | + oauth |
12 | unity >= 6.90.0 |
13 | unity-extras >= 6.90.0 |
14 | tdb >= 1.2.6) |
15 | |
16 | === modified file 'debian/changelog' |
17 | --- debian/changelog 2012-12-10 16:34:35 +0000 |
18 | +++ debian/changelog 2012-12-12 00:01:27 +0000 |
19 | @@ -1,3 +1,10 @@ |
20 | +unity-lens-music (6.8.1daily12.12.05-0ubuntu2) UNRELEASED; urgency=low |
21 | + |
22 | + * debian/control: |
23 | + - added libsoup2.4-dev and liboauth-dev build dependencies |
24 | + |
25 | + -- Alejandro J. Cura <alecu@canonical.com> Tue, 11 Dec 2012 20:54:55 -0300 |
26 | + |
27 | unity-lens-music (6.8.1daily12.12.05-0ubuntu1) raring; urgency=low |
28 | |
29 | [ Michael Terry ] |
30 | |
31 | === modified file 'debian/control' |
32 | --- debian/control 2012-12-10 16:34:35 +0000 |
33 | +++ debian/control 2012-12-12 00:01:27 +0000 |
34 | @@ -11,6 +11,8 @@ |
35 | libglib2.0-dev (>= 2.27), |
36 | libgee-dev, |
37 | libjson-glib-dev, |
38 | + libsoup2.4-dev, |
39 | + liboauth-dev, |
40 | libdee-dev (>= 1.0.7), |
41 | libsqlite3-dev (>= 3.7.7), |
42 | libunity-dev (>= 6.90.0), |
43 | |
44 | === modified file 'po/POTFILES.skip' |
45 | --- po/POTFILES.skip 2012-08-17 07:56:11 +0000 |
46 | +++ po/POTFILES.skip 2012-12-12 00:01:27 +0000 |
47 | @@ -5,3 +5,5 @@ |
48 | src/banshee-scope.c |
49 | src/rhythmbox-scope.c |
50 | src/musicstore-scope.c |
51 | +src/ubuntuone-webservices.c |
52 | +tests/unit/ubuntuone-webservices.c |
53 | |
54 | === modified file 'src/Makefile.am' |
55 | --- src/Makefile.am 2012-11-14 14:45:52 +0000 |
56 | +++ src/Makefile.am 2012-12-12 00:01:27 +0000 |
57 | @@ -69,6 +69,8 @@ |
58 | |
59 | unity_musicstore_daemon_VALAFLAGS = \ |
60 | --pkg json-glib-1.0 \ |
61 | + --pkg oauth \ |
62 | + --pkg libsoup-2.4 \ |
63 | $(unity_music_daemon_VALAFLAGS) |
64 | |
65 | unity_musicstore_daemon_LDADD = \ |
66 | @@ -88,7 +90,9 @@ |
67 | musicstore-filter-parser-genre.vala \ |
68 | musicstore-scope.vala \ |
69 | simple-scope.vala \ |
70 | - track.vala |
71 | + track.vala \ |
72 | + ubuntuone-webservices.vala \ |
73 | + $(NULL) |
74 | |
75 | unity_musicstore_daemon_SOURCES = \ |
76 | $(unity_musicstore_daemon_VALASOURCES:.vala=.c) \ |
77 | |
78 | === added file 'src/oauth.vapi' |
79 | --- src/oauth.vapi 1970-01-01 00:00:00 +0000 |
80 | +++ src/oauth.vapi 2012-12-12 00:01:27 +0000 |
81 | @@ -0,0 +1,19 @@ |
82 | +/* Minimal vapi for OAuth */ |
83 | +[CCode (cheader_filename="oauth.h")] |
84 | +namespace OAuth { |
85 | + |
86 | + [CCode (cheader_filename="oauth.h", cprefix="OA_")] |
87 | + public enum Method { |
88 | + HMAC, |
89 | + RSA, |
90 | + PLAINTEXT |
91 | + } |
92 | + |
93 | + [CCode (cheader_filename="oauth.h", cprefix="oauth_")] |
94 | + public string sign_url2(string url, out string postargs, |
95 | + OAuth.Method method, |
96 | + string? http_method, |
97 | + string c_key, string c_secret, |
98 | + string t_key, string t_secret); |
99 | + |
100 | +} |
101 | |
102 | === added file 'src/ubuntuone-webservices.vala' |
103 | --- src/ubuntuone-webservices.vala 1970-01-01 00:00:00 +0000 |
104 | +++ src/ubuntuone-webservices.vala 2012-12-12 00:01:27 +0000 |
105 | @@ -0,0 +1,342 @@ |
106 | +/* |
107 | + * Copyright (C) 2012 Canonical Ltd |
108 | + * |
109 | + * This program is free software: you can redistribute it and/or modify |
110 | + * it under the terms of the GNU General Public License version 3 as |
111 | + * published by the Free Software Foundation. |
112 | + * |
113 | + * This program is distributed in the hope that it will be useful, |
114 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
115 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
116 | + * GNU General Public License for more details. |
117 | + * |
118 | + * You should have received a copy of the GNU General Public License |
119 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
120 | + * |
121 | + * Authored by Alejandro J. Cura <alecu@canonical.com> |
122 | + * |
123 | + */ |
124 | + |
125 | +using Soup; |
126 | + |
127 | +[DBus (name = "com.ubuntuone.CredentialsManagement")] |
128 | +interface CredentialsManagement : GLib.Object { |
129 | + public signal void credentials_found (HashTable <string, string> info); |
130 | + public signal void credentials_error (); |
131 | + |
132 | + [DBus (name = "find_credentials")] |
133 | + public abstract void find_credentials () throws IOError; |
134 | +} |
135 | + |
136 | +const string WEBAPI_URI = "https://edge.one.ubuntu.com/"; |
137 | +const string ACCOUNT_URI = WEBAPI_URI + "api/account"; |
138 | +const string PAYMENT_METHOD_URI = WEBAPI_URI + "music-store/api/1/user/retrieve-payment-method?purchase_sku=%s"; |
139 | +const string PURCHASE_WITH_DEFAULT_PAYMENT_URI = WEBAPI_URI + "music-store/api/1/user/purchase-with-default-payment?purchase_sku=%s&authentication=%s"; |
140 | +const string AUTHENTICATION_URI = "https://login.staging.ubuntu.com/api/1.1/authentications"; |
141 | +const string AUTHENTICATE_PARAMS = "ws.op=authenticate&token_name=Purchase_Token"; |
142 | +const int PASSWORD_CACHE_SECONDS = 300; |
143 | +const int PAYMENT_METHOD_CACHE_SECONDS = 120; |
144 | + |
145 | +namespace Ubuntuone.Webservice |
146 | +{ |
147 | + public errordomain PurchaseError |
148 | + { |
149 | + PURCHASE_ERROR, |
150 | + WRONG_PASSWORD_ERROR, |
151 | + UNSPECIFIED_ERROR |
152 | + } |
153 | + |
154 | + public class PurchaseService : GLib.Object |
155 | + { |
156 | + internal Soup.SessionAsync http_session; |
157 | + Soup.SessionAsync http_session_sso; |
158 | + CredentialsManagement credentials_management; |
159 | + public string nickname { get; private set; default = null; } |
160 | + public string email { get; private set; default = null; } |
161 | + public string selected_payment_method { get; internal set; default = null; } |
162 | + public string consumer_key { get; private set; default = null; } |
163 | + public string token { get; private set; default = null; } |
164 | + internal DateTime _selected_payment_method_expiry; |
165 | + DateTime _password_expiry; |
166 | + internal HashTable <string, string> _ubuntuone_credentials = null; |
167 | + |
168 | + construct { |
169 | + http_session = build_http_session (); |
170 | + http_session_sso = build_http_session (); |
171 | + |
172 | + reset_payment_method_cache (0); |
173 | + reset_password_cache (0); |
174 | + |
175 | + credentials_management = build_credentials_management (); |
176 | + } |
177 | + |
178 | + internal Soup.SessionAsync build_http_session () |
179 | + { |
180 | + var session = new Soup.SessionAsync (); |
181 | + session.user_agent = "%s/%s (libsoup)".printf("UbuntuOneMusicstoreLens", "1.0"); |
182 | + return session; |
183 | + } |
184 | + |
185 | + internal DateTime now () |
186 | + { |
187 | + return new DateTime.now_utc (); |
188 | + } |
189 | + |
190 | + internal bool expired_payment_method_cache () |
191 | + { |
192 | + return _selected_payment_method_expiry.compare (now ()) <= 0; |
193 | + } |
194 | + |
195 | + internal void reset_payment_method_cache (int seconds_from_now) |
196 | + { |
197 | + _selected_payment_method_expiry = now ().add_seconds (seconds_from_now); |
198 | + } |
199 | + |
200 | + internal void reset_password_cache (int seconds_from_now) |
201 | + { |
202 | + _password_expiry = now ().add_seconds (seconds_from_now); |
203 | + } |
204 | + |
205 | + internal virtual CredentialsManagement build_credentials_management () |
206 | + { |
207 | + try { |
208 | + return Bus.get_proxy_sync (BusType.SESSION, "com.ubuntuone.Credentials", |
209 | + "/credentials", DBusProxyFlags.DO_NOT_AUTO_START); |
210 | + } catch (IOError e) { |
211 | + error ("Can't connect to DBus: %s", e.message); |
212 | + } |
213 | + } |
214 | + |
215 | + public bool ready_to_purchase { |
216 | + get { return selected_payment_method != null; } |
217 | + } |
218 | + |
219 | + internal Json.Object parse_json (string json_string) throws GLib.Error |
220 | + { |
221 | + var parser = new Json.Parser(); |
222 | + parser.load_from_data(json_string, -1); |
223 | + return parser.get_root().get_object(); |
224 | + } |
225 | + |
226 | + internal void parse_account_json (string json_string) throws GLib.Error |
227 | + { |
228 | + var root_object = parse_json (json_string); |
229 | + nickname = root_object.get_string_member("nickname"); |
230 | + email = root_object.get_string_member("email"); |
231 | + } |
232 | + |
233 | + internal void parse_payment_method_json (string json_string) throws GLib.Error |
234 | + { |
235 | + var root_object = parse_json (json_string); |
236 | + Json.Object payload = root_object.get_object_member("payload"); |
237 | + selected_payment_method = payload.get_string_member("selected_payment_method"); |
238 | + } |
239 | + |
240 | + internal void parse_authentication_json (string json_string) throws GLib.Error |
241 | + { |
242 | + var root_object = parse_json (json_string); |
243 | + consumer_key = root_object.get_string_member("consumer_key"); |
244 | + token = root_object.get_string_member("token"); |
245 | + } |
246 | + |
247 | + internal string parse_purchase_json (string json_string) throws GLib.Error |
248 | + { |
249 | + var root_object = parse_json (json_string); |
250 | + Json.Object payload = root_object.get_object_member("payload"); |
251 | + return payload.get_string_member("open_url"); |
252 | + } |
253 | + |
254 | + internal virtual async void fetch_credentials () throws PurchaseError |
255 | + { |
256 | + PurchaseError error = null; |
257 | + |
258 | + ulong found_handler = credentials_management.credentials_found.connect ((credentials) => { |
259 | + _ubuntuone_credentials = credentials; |
260 | + debug ("got credentials"); |
261 | + fetch_credentials.callback (); |
262 | + }); |
263 | + ulong error_handler = credentials_management.credentials_error.connect (() => { |
264 | + error = new PurchaseError.PURCHASE_ERROR ("Can't get Ubuntu One tokens."); |
265 | + fetch_credentials.callback (); |
266 | + }); |
267 | + |
268 | + try { |
269 | + credentials_management.find_credentials (); |
270 | + yield; |
271 | + } catch (IOError e) { |
272 | + error = new PurchaseError.PURCHASE_ERROR ("Can't get Ubuntu One tokens: %s", e.message); |
273 | + } |
274 | + |
275 | + credentials_management.disconnect (found_handler); |
276 | + credentials_management.disconnect (error_handler); |
277 | + |
278 | + if (error != null) { |
279 | + debug ("Can't get Ubuntu One tokens: %s", error.message); |
280 | + throw error; |
281 | + } |
282 | + } |
283 | + |
284 | + string oauth_sign (string uri) |
285 | + { |
286 | + return OAuth.sign_url2(uri, null, |
287 | + OAuth.Method.PLAINTEXT, "GET", |
288 | + _ubuntuone_credentials["consumer_key"], |
289 | + _ubuntuone_credentials["consumer_secret"], |
290 | + _ubuntuone_credentials["token"], |
291 | + _ubuntuone_credentials["token_secret"]); |
292 | + } |
293 | + |
294 | + internal virtual async PurchaseError call_api (string method, string uri, out string response) |
295 | + { |
296 | + PurchaseError error = null; |
297 | + var signed_uri = oauth_sign (uri); |
298 | + var message = new Soup.Message (method, signed_uri); |
299 | + http_session.queue_message (message, (session, message) => { |
300 | + if (message.status_code != Soup.KnownStatusCode.OK) { |
301 | + debug ("Web request failed: HTTP %u %s - %s", |
302 | + message.status_code, message.reason_phrase, uri); |
303 | + error = new PurchaseError.PURCHASE_ERROR (message.reason_phrase); |
304 | + } |
305 | + call_api.callback (); |
306 | + }); |
307 | + yield; |
308 | + message.response_body.flatten (); |
309 | + response = (string) message.response_body.data; |
310 | + return error; |
311 | + } |
312 | + |
313 | + internal virtual async void fetch_account () throws PurchaseError |
314 | + { |
315 | + string response; |
316 | + PurchaseError error = yield call_api ("GET", ACCOUNT_URI, out response); |
317 | + |
318 | + if (error != null) { |
319 | + debug ("Error while fetching U1 account: %s.", error.message); |
320 | + throw error; |
321 | + } |
322 | + |
323 | + try { |
324 | + parse_account_json (response); |
325 | + debug ("got account"); |
326 | + } catch (GLib.Error e) { |
327 | + debug ("Error while parsing U1 account: %s.", e.message); |
328 | + throw new PurchaseError.PURCHASE_ERROR (e.message); |
329 | + } |
330 | + } |
331 | + |
332 | + internal virtual async void fetch_payment_method (string purchase_sku) throws PurchaseError |
333 | + { |
334 | + string response; |
335 | + var uri = PAYMENT_METHOD_URI.printf (purchase_sku); |
336 | + PurchaseError error = yield call_api ("GET", uri, out response); |
337 | + |
338 | + if (error != null) { |
339 | + debug ("Error while fetching payment method: %s.", error.message); |
340 | + throw error; |
341 | + } |
342 | + |
343 | + try { |
344 | + parse_payment_method_json (response); |
345 | + debug ("got payment method"); |
346 | + } catch (GLib.Error e) { |
347 | + debug ("Error while getting payment method: %s.", e.message); |
348 | + throw new PurchaseError.PURCHASE_ERROR (e.message); |
349 | + } |
350 | + } |
351 | + |
352 | + internal virtual async void refetch_payment_info (string purchase_sku) throws PurchaseError |
353 | + { |
354 | + yield fetch_credentials (); |
355 | + yield fetch_account (); |
356 | + yield fetch_payment_method (purchase_sku); |
357 | + reset_payment_method_cache (PAYMENT_METHOD_CACHE_SECONDS); |
358 | + } |
359 | + |
360 | + public async void fetch_payment_info (string purchase_sku) throws PurchaseError |
361 | + { |
362 | + if (expired_payment_method_cache ()) |
363 | + { |
364 | + debug ("refetching"); |
365 | + yield refetch_payment_info (purchase_sku); |
366 | + } else { |
367 | + debug ("in cache"); |
368 | + } |
369 | + } |
370 | + |
371 | + internal virtual void _do_sso_webcall (Soup.Message message, string password) |
372 | + { |
373 | + var handler = http_session_sso.authenticate.connect ((session, message, auth, retrying) => { |
374 | + if (!retrying) { |
375 | + auth.authenticate (email, password); |
376 | + } |
377 | + }); |
378 | + http_session_sso.send_message (message); |
379 | + http_session_sso.disconnect (handler); |
380 | + } |
381 | + |
382 | + internal virtual string authenticated_sso_webcall (string method, string uri, string operation, string password) |
383 | + throws PurchaseError |
384 | + { |
385 | + var message = new Soup.Message (method, uri); |
386 | + message.set_request ("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, operation.data); |
387 | + _do_sso_webcall (message, password); |
388 | + if (message.status_code != Soup.KnownStatusCode.OK) { |
389 | + debug ("Authentication request failed: HTTP %u", message.status_code); |
390 | + debug ("Reason: %s", message.reason_phrase); |
391 | + if (message.status_code == Soup.KnownStatusCode.UNAUTHORIZED) { |
392 | + throw new PurchaseError.WRONG_PASSWORD_ERROR ("Wrong password"); |
393 | + } |
394 | + throw new PurchaseError.PURCHASE_ERROR (message.reason_phrase); |
395 | + } |
396 | + message.response_body.flatten (); |
397 | + return (string) message.response_body.data; |
398 | + } |
399 | + |
400 | + internal virtual string get_purchase_token (string password) throws PurchaseError |
401 | + { |
402 | + var result = authenticated_sso_webcall ("POST", AUTHENTICATION_URI, AUTHENTICATE_PARAMS, password); |
403 | + try { |
404 | + parse_authentication_json (result); |
405 | + } catch (GLib.Error e) { |
406 | + throw new PurchaseError.PURCHASE_ERROR (e.message); |
407 | + } |
408 | + return "%s:%s".printf (consumer_key, token); |
409 | + } |
410 | + |
411 | + internal virtual Soup.Message send_signed_webservice_call (string method, string uri) |
412 | + { |
413 | + var signed_uri = oauth_sign (uri); |
414 | + var message = new Soup.Message (method, signed_uri); |
415 | + http_session.send_message (message); |
416 | + return message; |
417 | + } |
418 | + |
419 | + internal virtual void purchase_with_default_payment (string sku, string purchase_token) throws PurchaseError |
420 | + { |
421 | + var uri = PURCHASE_WITH_DEFAULT_PAYMENT_URI.printf (sku, purchase_token); |
422 | + var message = send_signed_webservice_call ("GET", uri); |
423 | + |
424 | + if (message.status_code != Soup.KnownStatusCode.OK) { |
425 | + debug ("Purchase request failed: HTTP %u", message.status_code); |
426 | + debug ("Reason: %s", message.reason_phrase); |
427 | + throw new PurchaseError.PURCHASE_ERROR ("Purchase failed: %s".printf (message.reason_phrase)); |
428 | + } |
429 | + try { |
430 | + message.response_body.flatten (); |
431 | + var result = (string) message.response_body.data; |
432 | + var open_url = parse_purchase_json (result); |
433 | + if (open_url != "") { |
434 | + throw new PurchaseError.PURCHASE_ERROR (open_url); |
435 | + } |
436 | + } catch (GLib.Error e) { |
437 | + throw new PurchaseError.PURCHASE_ERROR (e.message); |
438 | + } |
439 | + } |
440 | + |
441 | + public void purchase (string album_id, string password) throws PurchaseError |
442 | + { |
443 | + var purchase_token = get_purchase_token (password); |
444 | + purchase_with_default_payment (album_id, purchase_token); |
445 | + } |
446 | + } |
447 | +} |
448 | |
449 | === modified file 'tests/unit/Makefile.am' |
450 | --- tests/unit/Makefile.am 2012-08-13 10:36:17 +0000 |
451 | +++ tests/unit/Makefile.am 2012-12-12 00:01:27 +0000 |
452 | @@ -1,4 +1,4 @@ |
453 | -check_PROGRAMS = test-rhythmbox-parser |
454 | +check_PROGRAMS = test-rhythmbox-parser test-ubuntuone-purchases |
455 | |
456 | TESTS = $(check_PROGRAMS) |
457 | |
458 | @@ -16,6 +16,9 @@ |
459 | --pkg gio-2.0 \ |
460 | --pkg gio-unix-2.0 \ |
461 | --pkg glib-2.0 \ |
462 | + --pkg json-glib-1.0 \ |
463 | + --pkg oauth \ |
464 | + --pkg libsoup-2.4 \ |
465 | --vapidir $(top_srcdir)/src \ |
466 | --pkg tdb \ |
467 | --pkg unity \ |
468 | @@ -41,6 +44,13 @@ |
469 | $(top_srcdir)/src/track.vala \ |
470 | $(NULL) |
471 | |
472 | +test_ubuntuone_purchases_LDADD = $(test_libs) |
473 | + |
474 | +test_ubuntuone_purchases_SOURCES = \ |
475 | + test-ubuntuone-purchases.vala \ |
476 | + $(top_srcdir)/src/ubuntuone-webservices.vala \ |
477 | + $(NULL) |
478 | + |
479 | EXTRA_DIST = assertions.vapi |
480 | CLEANFILES = *.stamp |
481 | |
482 | |
483 | === added file 'tests/unit/test-ubuntuone-purchases.vala' |
484 | --- tests/unit/test-ubuntuone-purchases.vala 1970-01-01 00:00:00 +0000 |
485 | +++ tests/unit/test-ubuntuone-purchases.vala 2012-12-12 00:01:27 +0000 |
486 | @@ -0,0 +1,761 @@ |
487 | +/* |
488 | + * Copyright (C) 2012 Canonical Ltd |
489 | + * |
490 | + * This program is free software: you can redistribute it and/or modify |
491 | + * it under the terms of the GNU General Public License version 3 as |
492 | + * published by the Free Software Foundation. |
493 | + * |
494 | + * This program is distributed in the hope that it will be useful, |
495 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
496 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
497 | + * GNU General Public License for more details. |
498 | + * |
499 | + * You should have received a copy of the GNU General Public License |
500 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
501 | + * |
502 | + * Authored by Alejandro J. Cura <alecu@canonical.com> |
503 | + * |
504 | + */ |
505 | + |
506 | +using Assertions; |
507 | +using Gee; |
508 | +using GLib; |
509 | +using Ubuntuone.Webservice; |
510 | + |
511 | + |
512 | +const string FAKE_URL = "http://fake/url"; |
513 | +const string FAKE_PASSWORD = "PezEspada"; |
514 | +const string FAKE_SKU = "fake_store:fake_album:id"; |
515 | +const string FAKE_TOKEN = "a fake token"; |
516 | + |
517 | +const string BROKEN_JSON = """ |
518 | + { |
519 | + "a": "b", |
520 | + $!@$# BROKEN! |
521 | +"""; |
522 | + |
523 | +const string FAKE_JSON_ACCOUNT = """ |
524 | + { |
525 | + "username": "mrbean", |
526 | + "nickname": "Mr. Bean", |
527 | + "email": "mr@be.an" |
528 | + } |
529 | +"""; |
530 | + |
531 | + |
532 | +const string FAKE_JSON_PAYMENT_METHOD = """ |
533 | + { |
534 | + "meta": { |
535 | + "status": "Ok" |
536 | + }, |
537 | + "payload": { |
538 | + "open_url": "", |
539 | + "user_email": "mr@be.an", |
540 | + "selected_payment_method": "Visa 1234" |
541 | + } |
542 | + } |
543 | +"""; |
544 | + |
545 | + |
546 | +const string FAKE_JSON_AUTHENTICATION = """ |
547 | + { |
548 | + "consumer_secret": "fake_secret", |
549 | + "token": "fake_token", |
550 | + "consumer_key": "fake_consumer_key", |
551 | + "name": "FAKE_TOKEN_NAME", |
552 | + "token_secret": "fake_token_secret" |
553 | + } |
554 | +"""; |
555 | + |
556 | +const string FAKE_JSON_PURCHASE = """ |
557 | + { |
558 | + "meta": { |
559 | + "status": "Ok" |
560 | + }, |
561 | + "payload": { |
562 | + "order_id": "1111", |
563 | + "order_status": "OK", |
564 | + "order_detail": "TBD", |
565 | + "open_url": "" |
566 | + } |
567 | + } |
568 | +"""; |
569 | + |
570 | + |
571 | +const string FAKE_JSON_PURCHASE_FAILURE = """ |
572 | + { |
573 | + "meta": { |
574 | + "status": "Ok" |
575 | + }, |
576 | + "payload": { |
577 | + "order_id": "1111", |
578 | + "order_status": "PAYMENT_FAILURE", |
579 | + "order_detail": "TBD", |
580 | + "open_url": "http://slashdot.org/" |
581 | + } |
582 | + } |
583 | +"""; |
584 | + |
585 | + |
586 | +public class Main |
587 | +{ |
588 | + public static int main (string[] args) |
589 | + { |
590 | + int result = run_tests (args); |
591 | + // irl_test (); |
592 | + return result; |
593 | + } |
594 | + |
595 | + static int run_tests (string[] args) |
596 | + { |
597 | + Test.init (ref args); |
598 | + |
599 | + Test.add_data_func ("/Unit/PurchaseChecker/ParseAccount", test_parse_account); |
600 | + Test.add_data_func ("/Unit/PurchaseChecker/ParseBrokenAccount", test_parse_broken_account); |
601 | + Test.add_data_func ("/Unit/PurchaseChecker/ParsePaymentMethod", test_parse_payment_method); |
602 | + Test.add_data_func ("/Unit/PurchaseChecker/ParseAuthentication", test_parse_authentication); |
603 | + Test.add_data_func ("/Unit/PurchaseChecker/ReadyToPurchase", test_ready_to_purchase); |
604 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentInvalidated", test_fetch_payment_invalidated); |
605 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentCached", test_fetch_payment_cached); |
606 | + Test.add_data_func ("/Unit/PurchaseChecker/ReFetchPayment", test_refetch_payment); |
607 | + Test.add_data_func ("/Unit/PurchaseChecker/ReFetchPaymentSetsCache", test_refetch_payment_sets_cache); |
608 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchCredentials", test_fetch_credentials); |
609 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchCredentialsFails", test_fetch_credentials_fails); |
610 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchCredentialsFailsExtra", test_fetch_credentials_fails_extra); |
611 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchAccount", test_fetch_account); |
612 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchAccountFails", test_fetch_account_http_failure); |
613 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchAccountBrokenJson", test_fetch_account_wrong_json); |
614 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchPayment", test_fetch_payment); |
615 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentFails", test_fetch_payment_http_failure); |
616 | + Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentBrokenJson", test_fetch_payment_wrong_json); |
617 | + Test.add_data_func ("/Unit/PurchaseChecker/Purchase", test_purchase); |
618 | + Test.add_data_func ("/Unit/PurchaseChecker/GetPurchaseToken", test_get_purchase_token); |
619 | + Test.add_data_func ("/Unit/PurchaseChecker/GetPurchaseTokenBrokenJson", test_get_purchase_token_broken_json); |
620 | + Test.add_data_func ("/Unit/PurchaseChecker/SsoWebcall", test_authenticated_sso_webcall); |
621 | + Test.add_data_func ("/Unit/PurchaseChecker/SsoWebcallFails", test_authenticated_sso_webcall_fails); |
622 | + Test.add_data_func ("/Unit/PurchaseChecker/SsoWebcallWrongPassword", test_authenticated_sso_webcall_wrong_password); |
623 | + Test.add_data_func ("/Unit/PurchaseChecker/PurchaseDefaultPayment", test_purchase_with_default_payment); |
624 | + Test.add_data_func ("/Unit/PurchaseChecker/PurchaseDefaultPaymentBroken", test_purchase_with_default_payment_broken_json); |
625 | + Test.add_data_func ("/Unit/PurchaseChecker/PurchaseConnectionFails", test_purchase_with_default_payment_connection_fails); |
626 | + Test.add_data_func ("/Unit/PurchaseChecker/PurchaseDefaultPaymentFails", test_purchase_with_default_payment_fails); |
627 | + |
628 | + return Test.run (); |
629 | + } |
630 | + |
631 | + public static bool run_with_timeout (MainLoop ml, uint timeout_ms) |
632 | + { |
633 | + bool timeout_reached = false; |
634 | + var t_id = Timeout.add (timeout_ms, () => { |
635 | + timeout_reached = true; |
636 | + debug ("Timeout reached"); |
637 | + ml.quit (); |
638 | + return false; |
639 | + }); |
640 | + |
641 | + ml.run (); |
642 | + |
643 | + if (!timeout_reached) Source.remove (t_id); |
644 | + |
645 | + return !timeout_reached; |
646 | + } |
647 | + |
648 | + |
649 | + async void irl_test_async () |
650 | + { |
651 | + var purchase_service = new PurchaseService(); |
652 | + var purchase_sku = "1"; |
653 | + try { |
654 | + yield purchase_service.fetch_payment_info (purchase_sku); |
655 | + debug ("data was available: %s %s %s", purchase_service.nickname, purchase_service.email, purchase_service.selected_payment_method); |
656 | + var real_user_password = "****fake_password_in_bzr***"; |
657 | + purchase_service.purchase (purchase_sku, real_user_password); |
658 | + debug ("purchase completed."); |
659 | + } catch (PurchaseError e) { |
660 | + debug ("got purchase error: %s", e.message); |
661 | + } |
662 | + } |
663 | + |
664 | + static void irl_test () |
665 | + { |
666 | + var loop = new GLib.MainLoop (); |
667 | + var m = new Main(); |
668 | + Idle.add (() => { |
669 | + m.irl_test_async.begin ((obj, res) => { |
670 | + m.irl_test_async.end (res); |
671 | + loop.quit (); |
672 | + }); |
673 | + return false; |
674 | + }); |
675 | + assert (run_with_timeout (loop, 10000)); |
676 | + } |
677 | + |
678 | + class FakeCredentialsManagement : GLib.Object, CredentialsManagement |
679 | + { |
680 | + private HashTable <string, string> fake_credentials; |
681 | + |
682 | + construct |
683 | + { |
684 | + fake_credentials = new HashTable <string, string> (str_hash, str_equal); |
685 | + fake_credentials["token"] = "fake_token"; |
686 | + } |
687 | + |
688 | + public void find_credentials () throws IOError |
689 | + { |
690 | + Idle.add (() => { |
691 | + credentials_found (fake_credentials); |
692 | + return false; |
693 | + }); |
694 | + } |
695 | + } |
696 | + |
697 | + class FailingCredentialsManagement : GLib.Object, CredentialsManagement |
698 | + { |
699 | + public void find_credentials () throws IOError |
700 | + { |
701 | + Idle.add (() => { |
702 | + credentials_error (); |
703 | + return false; |
704 | + }); |
705 | + } |
706 | + } |
707 | + |
708 | + class ExtraFailingCredentialsManagement : GLib.Object, CredentialsManagement |
709 | + { |
710 | + public void find_credentials () throws IOError |
711 | + { |
712 | + throw new IOError.INVALID_DATA ("Miscellanea Errora"); |
713 | + } |
714 | + } |
715 | + |
716 | + class BaseTestPurchaseService : PurchaseService |
717 | + { |
718 | + internal override CredentialsManagement build_credentials_management () |
719 | + { |
720 | + return new FakeCredentialsManagement (); |
721 | + } |
722 | + } |
723 | + |
724 | + class FailingCredentialsPurchaseService : PurchaseService |
725 | + { |
726 | + internal override CredentialsManagement build_credentials_management () |
727 | + { |
728 | + return new FailingCredentialsManagement (); |
729 | + } |
730 | + } |
731 | + |
732 | + class ExtraFailingCredentialsPurchaseService : PurchaseService |
733 | + { |
734 | + internal override CredentialsManagement build_credentials_management () |
735 | + { |
736 | + return new ExtraFailingCredentialsManagement (); |
737 | + } |
738 | + } |
739 | + |
740 | + private static void test_parse_account () |
741 | + { |
742 | + var purchase_service = new BaseTestPurchaseService (); |
743 | + try { |
744 | + purchase_service.parse_account_json (FAKE_JSON_ACCOUNT); |
745 | + } catch (GLib.Error e) |
746 | + { |
747 | + assert_not_reached (); |
748 | + } |
749 | + assert_cmpstr (purchase_service.nickname, OperatorType.EQUAL, "Mr. Bean"); |
750 | + assert_cmpstr (purchase_service.email, OperatorType.EQUAL, "mr@be.an"); |
751 | + } |
752 | + |
753 | + private static void test_parse_broken_account () |
754 | + { |
755 | + var purchase_service = new BaseTestPurchaseService (); |
756 | + try |
757 | + { |
758 | + purchase_service.parse_account_json (BROKEN_JSON); |
759 | + assert_not_reached (); |
760 | + } catch (GLib.Error e) |
761 | + { |
762 | + // This is *just* the error we are expecting, so do nothing! |
763 | + } |
764 | + } |
765 | + |
766 | + private static void test_parse_payment_method () |
767 | + { |
768 | + var purchase_service = new BaseTestPurchaseService (); |
769 | + try |
770 | + { |
771 | + purchase_service.parse_payment_method_json (FAKE_JSON_PAYMENT_METHOD); |
772 | + } catch (GLib.Error e) |
773 | + { |
774 | + assert_not_reached (); |
775 | + } |
776 | + assert_cmpstr (purchase_service.selected_payment_method, OperatorType.EQUAL, "Visa 1234"); |
777 | + } |
778 | + |
779 | + private static void test_parse_authentication () |
780 | + { |
781 | + var purchase_service = new BaseTestPurchaseService (); |
782 | + |
783 | + try |
784 | + { |
785 | + purchase_service.parse_authentication_json (FAKE_JSON_AUTHENTICATION); |
786 | + } catch (GLib.Error e) |
787 | + { |
788 | + assert_not_reached (); |
789 | + } |
790 | + assert_cmpstr (purchase_service.consumer_key, OperatorType.EQUAL, "fake_consumer_key"); |
791 | + assert_cmpstr (purchase_service.token, OperatorType.EQUAL, "fake_token"); |
792 | + } |
793 | + |
794 | + private static void test_ready_to_purchase () |
795 | + { |
796 | + var purchase_service = new BaseTestPurchaseService (); |
797 | + assert (purchase_service.ready_to_purchase == false); |
798 | + purchase_service.selected_payment_method = "visa 1234"; |
799 | + assert (purchase_service.ready_to_purchase == true); |
800 | + } |
801 | + |
802 | + class TestFetchPurchaseService : BaseTestPurchaseService { |
803 | + internal bool fetch_called = false; |
804 | + internal override async void refetch_payment_info (string purchase_sku) |
805 | + { |
806 | + fetch_called = true; |
807 | + } |
808 | + } |
809 | + |
810 | + private static void test_fetch_payment_invalidated () |
811 | + { |
812 | + var purchase_service = new TestFetchPurchaseService (); |
813 | + purchase_service._selected_payment_method_expiry = new DateTime.now_utc (); |
814 | + purchase_service.fetch_payment_info ("fake_sku"); |
815 | + assert (purchase_service.fetch_called); |
816 | + } |
817 | + |
818 | + private static void test_fetch_payment_cached () |
819 | + { |
820 | + var purchase_service = new TestFetchPurchaseService (); |
821 | + purchase_service._selected_payment_method_expiry = new DateTime.now_utc ().add_seconds (60); |
822 | + purchase_service.fetch_payment_info ("fake_sku"); |
823 | + assert (purchase_service.fetch_called == false); |
824 | + } |
825 | + |
826 | + class TestRefetchPurchaseService : BaseTestPurchaseService { |
827 | + internal bool credentials_fetched = false; |
828 | + internal bool account_fetched = false; |
829 | + internal bool payment_method_fetched = false; |
830 | + |
831 | + internal override async void fetch_credentials () |
832 | + { |
833 | + Idle.add (() => { |
834 | + credentials_fetched = true; |
835 | + fetch_credentials.callback (); |
836 | + return false; |
837 | + }); |
838 | + yield; |
839 | + } |
840 | + internal override async void fetch_account () |
841 | + { |
842 | + Idle.add (() => { |
843 | + account_fetched = true; |
844 | + fetch_account.callback (); |
845 | + return false; |
846 | + }); |
847 | + yield; |
848 | + } |
849 | + internal override async void fetch_payment_method (string purchase_sku) |
850 | + { |
851 | + Idle.add (() => { |
852 | + payment_method_fetched = true; |
853 | + fetch_payment_method.callback (); |
854 | + return false; |
855 | + }); |
856 | + yield; |
857 | + } |
858 | + } |
859 | + |
860 | + private static void test_refetch_payment () |
861 | + { |
862 | + var purchase_service = new TestRefetchPurchaseService (); |
863 | + |
864 | + MainLoop mainloop = new MainLoop (); |
865 | + purchase_service.refetch_payment_info.begin("fake_sku", (obj, res) => { |
866 | + mainloop.quit (); |
867 | + try { |
868 | + purchase_service.refetch_payment_info.end (res); |
869 | + } catch (PurchaseError e) { |
870 | + error ("Can't fetch payment info: %s", e.message); |
871 | + } |
872 | + }); |
873 | + assert (run_with_timeout (mainloop, 1000)); |
874 | + |
875 | + assert (purchase_service.credentials_fetched); |
876 | + assert (purchase_service.account_fetched); |
877 | + assert (purchase_service.payment_method_fetched); |
878 | + } |
879 | + |
880 | + private static void test_refetch_payment_sets_cache () |
881 | + { |
882 | + var purchase_service = new TestRefetchPurchaseService (); |
883 | + |
884 | + assert (true == purchase_service.expired_payment_method_cache ()); |
885 | + |
886 | + MainLoop mainloop = new MainLoop (); |
887 | + purchase_service.refetch_payment_info.begin("fake_sku", (obj, res) => { |
888 | + mainloop.quit (); |
889 | + try { |
890 | + purchase_service.refetch_payment_info.end (res); |
891 | + } catch (PurchaseError e) { |
892 | + error ("Can't fetch payment info: %s", e.message); |
893 | + } |
894 | + }); |
895 | + assert (run_with_timeout (mainloop, 1000)); |
896 | + |
897 | + assert (false == purchase_service.expired_payment_method_cache ()); |
898 | + } |
899 | + |
900 | + private static void test_fetch_credentials () |
901 | + { |
902 | + var purchase_service = new BaseTestPurchaseService (); |
903 | + |
904 | + MainLoop mainloop = new MainLoop (); |
905 | + purchase_service.fetch_credentials.begin((obj, res) => { |
906 | + mainloop.quit (); |
907 | + try { |
908 | + purchase_service.fetch_credentials.end (res); |
909 | + } catch (PurchaseError e) { |
910 | + error ("Can't fetch credentials: %s", e.message); |
911 | + } |
912 | + }); |
913 | + assert (run_with_timeout (mainloop, 1000)); |
914 | + |
915 | + assert (purchase_service._ubuntuone_credentials != null); |
916 | + } |
917 | + |
918 | + private static void test_fetch_credentials_fails () |
919 | + { |
920 | + bool failed = false; |
921 | + var purchase_service = new FailingCredentialsPurchaseService (); |
922 | + |
923 | + MainLoop mainloop = new MainLoop (); |
924 | + purchase_service.fetch_credentials.begin((obj, res) => { |
925 | + mainloop.quit (); |
926 | + try { |
927 | + purchase_service.fetch_credentials.end (res); |
928 | + } catch (PurchaseError e) { |
929 | + failed = true; |
930 | + } |
931 | + }); |
932 | + assert (run_with_timeout (mainloop, 1000)); |
933 | + |
934 | + assert (failed); |
935 | + } |
936 | + |
937 | + private static void test_fetch_credentials_fails_extra () |
938 | + { |
939 | + bool failed = false; |
940 | + var purchase_service = new ExtraFailingCredentialsPurchaseService (); |
941 | + |
942 | + MainLoop mainloop = new MainLoop (); |
943 | + purchase_service.fetch_credentials.begin((obj, res) => { |
944 | + mainloop.quit (); |
945 | + try { |
946 | + purchase_service.fetch_credentials.end (res); |
947 | + } catch (PurchaseError e) { |
948 | + failed = true; |
949 | + } |
950 | + }); |
951 | + assert (run_with_timeout (mainloop, 1000)); |
952 | + |
953 | + assert (failed); |
954 | + } |
955 | + |
956 | + class FakeWebcallPurchaseService : BaseTestPurchaseService |
957 | + { |
958 | + internal string used_uri = null; |
959 | + internal int status_code; |
960 | + internal string found_json; |
961 | + |
962 | + internal FakeWebcallPurchaseService (int _status_code, string _found_json) { |
963 | + status_code = _status_code; |
964 | + found_json = _found_json; |
965 | + } |
966 | + |
967 | + internal override async PurchaseError call_api (string method, string uri, out string response) |
968 | + { |
969 | + PurchaseError error = null; |
970 | + Idle.add (() => { |
971 | + call_api.callback (); |
972 | + return false; |
973 | + }); |
974 | + yield; |
975 | + response = found_json; |
976 | + if (status_code != Soup.KnownStatusCode.OK) { |
977 | + error = new PurchaseError.PURCHASE_ERROR ("fake error"); |
978 | + } |
979 | + return error; |
980 | + } |
981 | + |
982 | + internal override void _do_sso_webcall (Soup.Message message, string password) |
983 | + { |
984 | + message.response_body.append (Soup.MemoryUse.STATIC, found_json.data); |
985 | + message.status_code = status_code; |
986 | + } |
987 | + |
988 | + internal override Soup.Message send_signed_webservice_call (string method, string uri) |
989 | + { |
990 | + var message = new Soup.Message (method, uri); |
991 | + message.response_body.append (Soup.MemoryUse.STATIC, found_json.data); |
992 | + message.status_code = status_code; |
993 | + return message; |
994 | + } |
995 | + } |
996 | + |
997 | + private static FakeWebcallPurchaseService _test_fetch_account (int _status_code, string _found_json) throws PurchaseError |
998 | + { |
999 | + var purchase_service = new FakeWebcallPurchaseService (_status_code, _found_json); |
1000 | + purchase_service._ubuntuone_credentials = new HashTable <string, string> (str_hash, str_equal); |
1001 | + |
1002 | + PurchaseError error = null; |
1003 | + MainLoop mainloop = new MainLoop (); |
1004 | + purchase_service.fetch_account.begin((obj, res) => { |
1005 | + mainloop.quit (); |
1006 | + try { |
1007 | + purchase_service.fetch_account.end (res); |
1008 | + } catch (PurchaseError e) { |
1009 | + error = e; |
1010 | + } |
1011 | + }); |
1012 | + assert (run_with_timeout (mainloop, 1000)); |
1013 | + if (error != null) { |
1014 | + throw error; |
1015 | + } |
1016 | + return purchase_service; |
1017 | + } |
1018 | + |
1019 | + private static void test_fetch_account () |
1020 | + { |
1021 | + try { |
1022 | + var purchase_service = _test_fetch_account (Soup.KnownStatusCode.OK, FAKE_JSON_ACCOUNT); |
1023 | + assert_cmpstr (purchase_service.nickname, OperatorType.EQUAL, "Mr. Bean"); |
1024 | + assert_cmpstr (purchase_service.email, OperatorType.EQUAL, "mr@be.an"); |
1025 | + } catch (PurchaseError e) { |
1026 | + assert_not_reached (); |
1027 | + } |
1028 | + } |
1029 | + |
1030 | + private static void test_fetch_account_http_failure () |
1031 | + { |
1032 | + try { |
1033 | + _test_fetch_account (Soup.KnownStatusCode.INTERNAL_SERVER_ERROR, FAKE_JSON_ACCOUNT); |
1034 | + assert_not_reached (); |
1035 | + } catch (PurchaseError e) { |
1036 | + // Expected error |
1037 | + } |
1038 | + } |
1039 | + |
1040 | + private static void test_fetch_account_wrong_json () |
1041 | + { |
1042 | + try { |
1043 | + _test_fetch_account (Soup.KnownStatusCode.OK, BROKEN_JSON); |
1044 | + assert_not_reached (); |
1045 | + } catch (PurchaseError e) { |
1046 | + // Expected error |
1047 | + } |
1048 | + } |
1049 | + |
1050 | + private static FakeWebcallPurchaseService _test_fetch_payment (int _status_code, string _found_json) throws PurchaseError |
1051 | + { |
1052 | + var purchase_service = new FakeWebcallPurchaseService (_status_code, _found_json); |
1053 | + purchase_service._ubuntuone_credentials = new HashTable <string, string> (str_hash, str_equal); |
1054 | + |
1055 | + PurchaseError error = null; |
1056 | + MainLoop mainloop = new MainLoop (); |
1057 | + purchase_service.fetch_payment_method.begin("fake_sku", (obj, res) => { |
1058 | + mainloop.quit (); |
1059 | + try { |
1060 | + purchase_service.fetch_payment_method.end (res); |
1061 | + } catch (PurchaseError e) { |
1062 | + error = e; |
1063 | + } |
1064 | + }); |
1065 | + assert (run_with_timeout (mainloop, 1000)); |
1066 | + if (error != null) { |
1067 | + throw error; |
1068 | + } |
1069 | + return purchase_service; |
1070 | + } |
1071 | + |
1072 | + private static void test_fetch_payment () |
1073 | + { |
1074 | + try { |
1075 | + var purchase_service = _test_fetch_payment (Soup.KnownStatusCode.OK, FAKE_JSON_PAYMENT_METHOD); |
1076 | + assert_cmpstr (purchase_service.selected_payment_method, OperatorType.EQUAL, "Visa 1234"); |
1077 | + } catch (PurchaseError e) { |
1078 | + assert_not_reached (); |
1079 | + } |
1080 | + } |
1081 | + |
1082 | + private static void test_fetch_payment_http_failure () |
1083 | + { |
1084 | + try { |
1085 | + _test_fetch_payment (Soup.KnownStatusCode.INTERNAL_SERVER_ERROR, FAKE_JSON_PAYMENT_METHOD); |
1086 | + assert_not_reached (); |
1087 | + } catch (PurchaseError e) { |
1088 | + // Expected error |
1089 | + } |
1090 | + } |
1091 | + |
1092 | + private static void test_fetch_payment_wrong_json () |
1093 | + { |
1094 | + try { |
1095 | + _test_fetch_payment (Soup.KnownStatusCode.OK, BROKEN_JSON); |
1096 | + assert_not_reached (); |
1097 | + } catch (PurchaseError e) { |
1098 | + // Expected error |
1099 | + } |
1100 | + } |
1101 | + |
1102 | + class TestPurchaseService : BaseTestPurchaseService |
1103 | + { |
1104 | + internal string password; |
1105 | + internal string sku; |
1106 | + internal string purchase_token; |
1107 | + |
1108 | + internal override string get_purchase_token (string password) |
1109 | + { |
1110 | + this.password = password; |
1111 | + return FAKE_TOKEN; |
1112 | + } |
1113 | + internal override void purchase_with_default_payment (string sku, string purchase_token) |
1114 | + { |
1115 | + this.sku = sku; |
1116 | + this.purchase_token = purchase_token; |
1117 | + } |
1118 | + } |
1119 | + |
1120 | + private static void test_purchase () |
1121 | + { |
1122 | + try { |
1123 | + var purchase_service = new TestPurchaseService (); |
1124 | + purchase_service.purchase (FAKE_SKU, FAKE_PASSWORD); |
1125 | + assert_cmpstr (purchase_service.password, OperatorType.EQUAL, FAKE_PASSWORD); |
1126 | + assert_cmpstr (purchase_service.sku, OperatorType.EQUAL, FAKE_SKU); |
1127 | + assert_cmpstr (purchase_service.purchase_token, OperatorType.EQUAL, FAKE_TOKEN); |
1128 | + } catch (Error e) { |
1129 | + assert_not_reached (); |
1130 | + } |
1131 | + } |
1132 | + |
1133 | + class FakePurchaseTokenService : BaseTestPurchaseService |
1134 | + { |
1135 | + string found_json; |
1136 | + |
1137 | + internal FakePurchaseTokenService (string _found_json) |
1138 | + { |
1139 | + found_json = _found_json; |
1140 | + } |
1141 | + |
1142 | + internal override string authenticated_sso_webcall (string method, string uri, string operation, string password) |
1143 | + throws PurchaseError |
1144 | + { |
1145 | + return found_json; |
1146 | + } |
1147 | + } |
1148 | + |
1149 | + private static void test_get_purchase_token () |
1150 | + { |
1151 | + var purchase_service = new FakePurchaseTokenService (FAKE_JSON_AUTHENTICATION); |
1152 | + var expected = "fake_consumer_key:fake_token"; |
1153 | + try { |
1154 | + var result = purchase_service.get_purchase_token (FAKE_PASSWORD); |
1155 | + assert_cmpstr (expected, OperatorType.EQUAL, result); |
1156 | + } catch (PurchaseError e) { |
1157 | + assert_not_reached (); |
1158 | + } |
1159 | + } |
1160 | + |
1161 | + private static void test_get_purchase_token_broken_json () |
1162 | + { |
1163 | + var purchase_service = new FakePurchaseTokenService (BROKEN_JSON); |
1164 | + try { |
1165 | + purchase_service.get_purchase_token (FAKE_PASSWORD); |
1166 | + assert_not_reached (); |
1167 | + } catch (PurchaseError e) { |
1168 | + assert (e is PurchaseError.PURCHASE_ERROR); |
1169 | + } |
1170 | + } |
1171 | + |
1172 | + private static void test_authenticated_sso_webcall () |
1173 | + { |
1174 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.OK, FAKE_JSON_AUTHENTICATION); |
1175 | + try { |
1176 | + var result = purchase_service.authenticated_sso_webcall ("FAKE", "http://some/where", "operation=wolf", FAKE_PASSWORD); |
1177 | + assert_cmpstr (FAKE_JSON_AUTHENTICATION, OperatorType.EQUAL, result); |
1178 | + } catch (PurchaseError e) { |
1179 | + assert_not_reached (); |
1180 | + } |
1181 | + } |
1182 | + |
1183 | + private static void test_authenticated_sso_webcall_fails () |
1184 | + { |
1185 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.INTERNAL_SERVER_ERROR, FAKE_JSON_AUTHENTICATION); |
1186 | + try { |
1187 | + purchase_service.authenticated_sso_webcall ("FAKE", "http://some/where", "operation=wolf", FAKE_PASSWORD); |
1188 | + assert_not_reached (); |
1189 | + } catch (Error e) { |
1190 | + assert (e is PurchaseError.PURCHASE_ERROR); |
1191 | + } |
1192 | + } |
1193 | + |
1194 | + private static void test_authenticated_sso_webcall_wrong_password () |
1195 | + { |
1196 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.UNAUTHORIZED, FAKE_JSON_AUTHENTICATION); |
1197 | + try { |
1198 | + purchase_service.authenticated_sso_webcall ("FAKE", "http://some/where", "operation=wolf", FAKE_PASSWORD); |
1199 | + assert_not_reached (); |
1200 | + } catch (Error e) { |
1201 | + assert (e is PurchaseError.WRONG_PASSWORD_ERROR); |
1202 | + } |
1203 | + } |
1204 | + |
1205 | + private static void test_purchase_with_default_payment () |
1206 | + { |
1207 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.OK, FAKE_JSON_PURCHASE); |
1208 | + try { |
1209 | + purchase_service.purchase_with_default_payment (FAKE_SKU, FAKE_TOKEN); |
1210 | + } catch (PurchaseError e) { |
1211 | + assert_not_reached (); |
1212 | + } |
1213 | + } |
1214 | + |
1215 | + private static void test_purchase_with_default_payment_broken_json () |
1216 | + { |
1217 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.OK, BROKEN_JSON); |
1218 | + try { |
1219 | + purchase_service.purchase_with_default_payment (FAKE_SKU, FAKE_TOKEN); |
1220 | + assert_not_reached (); |
1221 | + } catch (PurchaseError e) { |
1222 | + assert (e is PurchaseError.PURCHASE_ERROR); |
1223 | + } |
1224 | + } |
1225 | + |
1226 | + private static void test_purchase_with_default_payment_fails () |
1227 | + { |
1228 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.OK, FAKE_JSON_PURCHASE_FAILURE); |
1229 | + try { |
1230 | + purchase_service.purchase_with_default_payment (FAKE_SKU, FAKE_TOKEN); |
1231 | + assert_not_reached (); |
1232 | + } catch (PurchaseError e) { |
1233 | + assert (e is PurchaseError.PURCHASE_ERROR); |
1234 | + } |
1235 | + } |
1236 | + |
1237 | + private static void test_purchase_with_default_payment_connection_fails () |
1238 | + { |
1239 | + var purchase_service = new FakeWebcallPurchaseService (Soup.KnownStatusCode.INTERNAL_SERVER_ERROR, FAKE_JSON_PURCHASE_FAILURE); |
1240 | + try { |
1241 | + purchase_service.purchase_with_default_payment (FAKE_SKU, FAKE_TOKEN); |
1242 | + assert_not_reached (); |
1243 | + } catch (PurchaseError e) { |
1244 | + assert (e is PurchaseError.PURCHASE_ERROR); |
1245 | + } |
1246 | + } |
1247 | +} |
56 + public unowned string sign_url2(string url, string? postargs,
Looking at the docs, it says that you need to free the return, so s/unowned//, plus the postargs should be "out string postargs" as it's also a return (out params are nullable by default).
118 + internal Soup.SessionAsync http_session;
"internal" is a special keyword used when building libraries in vala (internal symbols are callable by other parts of the library, but not by the consumers), pls use private/public as appropriate.
177 + public string nickname {
178 + get { return _nickname; }
179 + }
+ other instances
No need for the extra private variable, use "public string x { get; private set; }"
243 + credentials_ management. find_credential s ();
244 + yield;
Wouldn't it be better if find_credentials() itself was async? Then you wouldn't need the signals and you could just return the credentials / throw an error in the async method.
275 + internal virtual async void fetch_account () throws PurchaseError
Nothing wrong here, nice async method ;)
285 + var result = (string) message. response_ body.data;
278 + queue_signed_ webservice_ call ("GET", ACCOUNT_URI, (session, message) => {
You're using this multiple times, perhaps add a helper async method that takes method + uri and returns flattened Soup.MessageBody?
.data could be null, no? Or does the flatten() cause possible null to become ""?
576 + return 0;
You should return result of Test.run ()
852 + MainLoop mainloop = new MainLoop (); service. refetch_ payment_ info.begin( (obj, res) => { service. refetch_ payment_ info.end (res);
853 + purchase_
854 + mainloop.quit ();
855 + try {
856 + purchase_
857 + } catch (PurchaseError e) {
858 + error ("Can't fetch payment info: %s", e.message);
859 + }
860 + });
861 + mainloop.run ();
I remember you mentioned on IRC that you weren't sure how to do async tests with vala, so yep, this is the way, usually we just have a wrapper for the mainloop.run() which also adds a timeout, so the test fails eventually if there's no response. Usually using "assert (run_with_timeout (mainloop));"