Merge lp:~michael.nelson/software-center/833982-purchased-app-not-available into lp:software-center

Proposed by Michael Nelson
Status: Merged
Merged at revision: 2670
Proposed branch: lp:~michael.nelson/software-center/833982-purchased-app-not-available
Merge into: lp:software-center
Diff against target: 588 lines (+354/-63)
4 files modified
.bzrignore (+5/-0)
README (+22/-5)
softwarecenter/db/update.py (+84/-32)
test/test_reinstall_purchased.py (+243/-26)
To merge this branch: bzr merge lp:~michael.nelson/software-center/833982-purchased-app-not-available
Reviewer Review Type Date Requested Status
Gary Lasker (community) Approve
Review via email: mp+88827@code.launchpad.net

Description of the change

This branch is part 1 of a fix for bug 917137 - Previous purchases empty in precise.

The cause of the bug seemed to be that the SoftwareCenterAgentParser was expecting a json object from the 1.0 'my subscriptions' api - where the attributes for the application related to each subscription were flattened into the one dict.

The 2.0 api does not do this, intentionally nesting the application attributes.

After chatting with mvo, we decided 2 parsers should be included - one for parsing applications (from 2.0 available apps call), the other for parsing subscriptions ("Purchased applications", from 2.0 subscriptions_for_me call).

Also done:
 * Removed the 'icon' attribute from the mapping, as we no-longer support it via the API (check with mvo).
 * Fixed get_desktop_categories

Testing
=======
cd test;make
Note: I get two failures (test_reviews, test_database) as well as python-coverage crashing while running the tests: http://paste.ubuntu.com/807433/

Anyway, still to do in a follow-up branch coming shortly:
 * Check if we can update PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME = "For Purchase" (value returned via API). How will this affect existing clients, if at all?
 * remove the need for the SCASubscriptionParser to both inherit from and encapsulate the SoftwareCenterAgentParser,
 * Filter results of the my_subscriptions call (only keep Complete ones), as currently it looks like this: http://people.canonical.com/~michaeln/tmp/833982-previous-purchases-working-but-dupes.png
 * Rename s/SoftwareCenterAgentParser/SCAApplicationParser for consistency
 * Replace the MockAvailableForMeItem/List with real PistonResponseObjects

To post a comment you must log in.
2676. By Michael Nelson

REFACTOR: replaced help/* in bzr ignore, updated requirement in README.

Revision history for this message
Gary Lasker (gary-lasker) wrote :

Hi Michael! This is really nice work and the additional changes that you are working on sound just right.

As we discussed in IRC earlier, there's one problem that I found in the current branch, and that is that with this change we getting an empty list for For Purchase apps. THis appears to be an issue with setting the "magic channel" to PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME in the SoftwareCenterAgentParser class.

I made a quick attempt to fix this in the following branch:

  lp:~gary-lasker/software-center/noodles-833982-purchased-app-not-available

You can see that I separated the two "magic channel" settings into the corresponding parser classes. This indeed restores the apps for purchase , but now I'm getting an empty list for the previous purchases, so it's not correct yet. Unfortunately, I had to leave for the day and and could not go further, but hopefully it's something simple and it won't be too hard to finish up tomorrow. In any case, I hope this branch is some help. I'll check in with you when I get back online in the morning.

Btw, I'm testing using the Oneiric apps for purchase by using:

  SOFTWARE_CENTER_DISTRO_CODENAME="oneiric" PYTHONPATH=. python ./software-center

Thanks!

2677. By Michael Nelson

RED: applications and purchased applications should have the correct magic channel.

2678. By Michael Nelson

GREEN: applications and purchased applications should have the correct magic channel.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

> Hi Michael! This is really nice work and the additional changes that you are
> working on sound just right.
>
> As we discussed in IRC earlier, there's one problem that I found in the
> current branch, and that is that with this change we getting an empty list for
> For Purchase apps. THis appears to be an issue with setting the "magic
> channel" to PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME in the
> SoftwareCenterAgentParser class.

Right - I'd not realised it was being set on the entry before instantiating the parser - thanks for the pointer.

>
> I made a quick attempt to fix this in the following branch:
>
> lp:~gary-lasker/software-center/noodles-833982-purchased-app-not-available
>
> You can see that I separated the two "magic channel" settings into the
> corresponding parser classes. This indeed restores the apps for purchase , but
> now I'm getting an empty list for the previous purchases, so it's not correct
> yet. Unfortunately, I had to leave for the day and and could not go further,
> but hopefully it's something simple and it won't be too hard to finish up
> tomorrow. In any case, I hope this branch is some help. I'll check in with you
> when I get back online in the morning.

Thanks - that helped. I've pushed some failing tests with r2677 then fixed the issue with r2678. Now I see both for purchase apps and my previously installed.

Revision history for this message
Gary Lasker (gary-lasker) wrote :

So, it's indeed fixed (nice, clean fix!) and already merged so I'll set this a approved. Thanks again for this and also thanks for the README dev setup instructions!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-05-27 08:25:15 +0000
3+++ .bzrignore 2012-01-18 09:00:32 +0000
4@@ -17,3 +17,8 @@
5 data/xapian/spelling.baseA
6 data/xapian/spelling.baseB
7 build/
8+test/.coverage
9+test/coverage_html
10+test/coverage_summary
11+test/output
12+help/*
13
14=== modified file 'README'
15--- README 2011-07-27 16:43:26 +0000
16+++ README 2012-01-18 09:00:32 +0000
17@@ -7,6 +7,23 @@
18
19 All non UI code must come with tests in the test/ subdirectoy.
20
21+To setup your development environment, you'll need to ensure the following
22+extra packages are installed:
23+
24+sudo apt-get install xvfb python-coverage python-mock python-aptdaemon.test \
25+ python-qt4 python-unittest2
26+sudo apt-get build-dep software-center
27+
28+You can then run tests with:
29+
30+cd test;make
31+
32+You can run a developer instance with:
33+
34+python setup.py build
35+./software-center
36+
37+
38 == query parser ==
39
40 The query parser understands :
41@@ -15,7 +32,7 @@
42
43 == aptdaemon ==
44 * the dbus limits for the system bus are rather low, this means that
45- adding <limit name="max_match_rules_per_connection">512</limit>
46+ adding <limit name="max_match_rules_per_connection">512</limit>
47 and using something bigger than 512 is a good idea
48
49 == environment ==
50@@ -54,7 +71,7 @@
51 SCPkgname - e.g. "gimp"
52
53 Additional .menu files can be added in:
54-/usr/share/app-install/menu.d
55+/usr/share/app-install/menu.d
56 that software-center will read and parse.
57
58 == XAPIAN ==
59@@ -66,9 +83,9 @@
60 AS - archive pocket (main)
61 AE - archive section (mail, base, ...)
62 AC - category (AudioVideo)
63-AM - MimeType (application/x-ogg)
64+AM - MimeType (application/x-ogg)
65 AT - type (Application)
66-AH - channel
67+AH - channel
68
69
70 The following values are used:
71@@ -89,5 +106,5 @@
72 XAPIAN_VALUE_PURCHASED_DATE - the data a for-pay app was purchased (only available after the software-center-agent server was queried)
73 XAPIAN_VALUE_SCREENSHOT_URL - a (optional) screenshot url that overrides the default
74 XAPIAN_VALUE_ICON_NEEDS_DOWNLOAD - icon needs to be fetched
75-XAPIAN_VALUE_THUMBNAIL_URL - thumbnail url
76+XAPIAN_VALUE_THUMBNAIL_URL - thumbnail url
77
78
79=== modified file 'softwarecenter/db/update.py'
80--- softwarecenter/db/update.py 2012-01-05 08:52:42 +0000
81+++ softwarecenter/db/update.py 2012-01-18 09:00:32 +0000
82@@ -26,6 +26,7 @@
83 import time
84
85 from gi.repository import GObject
86+from piston_mini_client import PistonResponseObject
87
88 from softwarecenter.utils import utf8
89
90@@ -131,6 +132,7 @@
91 def desktopf(self):
92 """ return the file that the AppInfo comes from """
93
94+
95 class SoftwareCenterAgentParser(AppInfoParserBase):
96 """ map the data we get from the software-center-agent """
97
98@@ -140,20 +142,17 @@
99 'Package' : 'package_name',
100 'Categories' : 'categories',
101 'Channel' : 'channel',
102- 'Deb-Line' : 'deb_line',
103 'Signing-Key-Id' : 'signing_key_id',
104 'License' : 'license',
105 'Date-Published' : 'date_published',
106- 'Purchased-Date' : 'purchase_date',
107- 'License-Key' : 'license_key',
108- 'License-Key-Path' : 'license_key_path',
109 'PPA' : 'archive_id',
110- 'Icon' : 'icon',
111 'Screenshot-Url' : 'screenshot_url',
112 'Thumbnail-Url' : 'thumbnail_url',
113 'Video-Url' : 'video_url',
114 'Icon-Url' : 'icon_url',
115 'Support-Url' : 'support_url',
116+ 'Description' : 'Description',
117+ 'Comment' : 'Comment',
118 }
119
120 # map from requested key to a static data element
121@@ -164,6 +163,7 @@
122 self.sca_entry = sca_entry
123 self.origin = "software-center-agent"
124 self._apply_exceptions()
125+
126 def _apply_exceptions(self):
127 # for items from the agent, we use the full-size screenshot for
128 # the thumbnail and scale it for display, this is done because
129@@ -172,25 +172,86 @@
130 not hasattr(self.sca_entry, "thumbnail_url")):
131 self.sca_entry.thumbnail_url = self.sca_entry.screenshot_url
132 if hasattr(self.sca_entry, "description"):
133- self.sca_entry.Comment = self.sca_entry.description.split("\n")[0]
134- self.sca_entry.Description = "\n".join(self.sca_entry.description.split("\n")[1:])
135+ self.sca_entry.Comment = self.sca_entry.description.split("\n")[0].strip()
136+ self.sca_entry.Description = "\n".join(
137+ self.sca_entry.description.split("\n")[1:]).strip()
138+ # WARNING: item.name needs to be different than
139+ # the item.name in the DB otherwise the DB
140+ # gets confused about (appname, pkgname) duplication
141+ self.sca_entry.name = utf8(_("%s (already purchased)")) % utf8(
142+ self.sca_entry.name)
143+
144+ # XXX 2012-01-16 bug=917109
145+ # We can remove these work-arounds once the above bug is fixed on
146+ # the server. Until then, we fake a channel here and empty category
147+ # to make the parser happy. Note: available_apps api call includes
148+ # these already, it's just the apps with subscriptions_for_me which
149+ # don't currently.
150+ self.sca_entry.channel = AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME
151+ if not hasattr(self.sca_entry, 'categories'):
152+ self.sca_entry.categories = ""
153+
154 def get_desktop(self, key, translated=True):
155 if key in self.STATIC_DATA:
156 return self.STATIC_DATA[key]
157 return getattr(self.sca_entry, self._apply_mapping(key))
158+
159 def get_desktop_categories(self):
160 try:
161 return ['DEPARTMENT:' + self.sca_entry.department[-1]] + self._get_desktop_list("Categories")
162 except:
163 return self._get_desktop_list("Categories")
164+
165 def has_option_desktop(self, key):
166 return (key in self.STATIC_DATA or
167 hasattr(self.sca_entry, self._apply_mapping(key)))
168+
169 @property
170 def desktopf(self):
171 return self.origin
172
173
174+class SCAPurchasedApplicationParser(SoftwareCenterAgentParser):
175+ """A subscription has its own attrs with a subset of the app attributes.
176+
177+ We inherit from SoftwareCenterAgentParser so that we get other methods for
178+ free, and we compose a SoftwareCenterAgentParser because we need the
179+ get_desktop method with the correct MAPPING. TODO: There must be a nicer
180+ way to organise this so that we don't need both inheritance and composition
181+ for a DRY implementation.
182+ """
183+
184+ def __init__(self, sca_subscription):
185+ # The sca_subscription is a PistonResponseObject, whereas any child
186+ # objects are normal Python dicts.
187+ self.sca_subscription = sca_subscription
188+ self.application_parser = SoftwareCenterAgentParser(
189+ PistonResponseObject.from_dict(sca_subscription.application))
190+ super(SCAPurchasedApplicationParser, self).__init__(
191+ PistonResponseObject.from_dict(sca_subscription.application))
192+ self.application_parser.sca_entry.channel = (
193+ PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME)
194+
195+ MAPPING = { 'Deb-Line' : 'deb_line',
196+ 'Purchased-Date' : 'purchase_date',
197+ 'License-Key' : 'license_key',
198+ 'License-Key-Path' : 'license_key_path',
199+ }
200+
201+ def get_desktop(self, key, translated=True):
202+ if self.application_parser.has_option_desktop(key):
203+ return self.application_parser.get_desktop(key, translated)
204+
205+ return getattr(self.sca_subscription, self._apply_mapping(key))
206+
207+ def has_option_desktop(self, key):
208+ subscription_has_option = hasattr(
209+ self.sca_subscription, self._apply_mapping(key))
210+ application_has_option = (
211+ self.application_parser.has_option_desktop(key))
212+ return subscription_has_option or application_has_option
213+
214+
215 class JsonTagSectionParser(AppInfoParserBase):
216
217 MAPPING = { 'Name' : 'application_name',
218@@ -505,15 +566,7 @@
219 # continue
220 # index the item
221 try:
222- # we fake a channel here
223- item.channel = PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME
224- # and empty category to make the parser happy
225- item.categories = ""
226- # WARNING: item.name needs to be different than
227- # the item.name in the DB otherwise the DB
228- # gets confused about (appname, pkgname) duplication
229- item.name = utf8(_("%s (already purchased)")) % utf8(item.name)
230- parser = SoftwareCenterAgentParser(item)
231+ parser = SCAPurchasedApplicationParser(item)
232 index_app_info_from_parser(parser, db_purchased, cache)
233 except Exception as e:
234 LOG.exception("error processing: %s " % e)
235@@ -560,8 +613,6 @@
236 while context.pending():
237 context.iteration()
238 try:
239- # magic channel
240- entry.channel = AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME
241 # now the normal parser
242 parser = SoftwareCenterAgentParser(entry)
243 index_app_info_from_parser(parser, db, cache)
244@@ -663,20 +714,21 @@
245 # date published
246 if parser.has_option_desktop("X-AppInstall-Date-Published"):
247 date_published = parser.get_desktop("X-AppInstall-Date-Published")
248- # strip the subseconds from the end of the published date string
249- date_published = str(date_published).split(".")[0]
250- doc.add_value(XapianValues.DATE_PUBLISHED,
251- date_published)
252- # we use the date published value for the cataloged time as well
253- if "catalogedtime" in axi_values:
254- LOG.debug(
255- ("pkgname: %s, date_published cataloged time is: %s" %
256- (pkgname, parser.get_desktop("date_published"))))
257- date_published_sec = time.mktime(
258- time.strptime(date_published,
259- "%Y-%m-%d %H:%M:%S"))
260- doc.add_value(axi_values["catalogedtime"],
261- xapian.sortable_serialise(date_published_sec))
262+ if date_published:
263+ # strip the subseconds from the end of the published date string
264+ date_published = str(date_published).split(".")[0]
265+ doc.add_value(XapianValues.DATE_PUBLISHED,
266+ date_published)
267+ # we use the date published value for the cataloged time as well
268+ if "catalogedtime" in axi_values:
269+ LOG.debug(
270+ ("pkgname: %s, date_published cataloged time is: %s" %
271+ (pkgname, parser.get_desktop("date_published"))))
272+ date_published_sec = time.mktime(
273+ time.strptime(date_published,
274+ "%Y-%m-%d %H:%M:%S"))
275+ doc.add_value(axi_values["catalogedtime"],
276+ xapian.sortable_serialise(date_published_sec))
277 # purchased date
278 if parser.has_option_desktop("X-AppInstall-Purchased-Date"):
279 date = parser.get_desktop("X-AppInstall-Purchased-Date")
280
281=== modified file 'test/test_reinstall_purchased.py'
282--- test/test_reinstall_purchased.py 2012-01-16 14:42:49 +0000
283+++ test/test_reinstall_purchased.py 2012-01-18 09:00:32 +0000
284@@ -7,28 +7,89 @@
285 import unittest
286 import xapian
287
288+from piston_mini_client import PistonResponseObject
289 from testutils import setup_test_env
290 setup_test_env()
291-from softwarecenter.enums import XapianValues
292+
293+from softwarecenter.enums import (XapianValues,
294+ AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME,
295+ PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME,
296+ )
297 from softwarecenter.db.database import StoreDatabase
298-from softwarecenter.db.update import add_from_purchased_but_needs_reinstall_data
299+from softwarecenter.db.update import (
300+ add_from_purchased_but_needs_reinstall_data,
301+ SCAPurchasedApplicationParser,
302+ SoftwareCenterAgentParser,
303+ )
304
305-# from
306-# https://wiki.canonical.com/Ubuntu/SoftwareCenter/10.10/Roadmap/SoftwareCenterAgent
307-AVAILABLE_FOR_ME_JSON = """
308-[
309- {
310- "archive_id": "mvo/private-test",
311- "deb_line": "deb https://username:randomp3atoken@private-ppa.launchpad.net/mvo/private-test/ubuntu maverick main #Personal access of username to private-test",
312- "purchase_price": "19.95",
313- "purchase_date": "2010-06-24 20:08:23",
314- "name": "Ubiteme",
315- "description": "One of the best strategy games you\'ll ever play!",
316- "package_name": "hellox",
317- "signing_key_id": "1024R/0EB12F05",
318- "series": {"natty": ["i386", "amd64"],
319- "maverick": ["i386", "amd64"],
320- "lucid": ["i386", "amd64"]}
321+# Example taken from running:
322+# PYTHONPATH=. utils/piston-helpers/piston_generic_helper.py --output=pickle \
323+# --debug --needs-auth SoftwareCenterAgentAPI subscriptions_for_me
324+# then:
325+# f = open('my_subscriptions.pickle')
326+# subscriptions = pickle.load(f)
327+# completed_subs = [subs for subs in subscriptions if subs.state=='Complete']
328+# completed_subs[0].__dict__
329+SUBSCRIPTIONS_FOR_ME_JSON = """
330+[
331+ {
332+ "deb_line": "deb https://username:random3atoken@private-ppa.launchpad.net/commercial-ppa-uploaders/photobomb/ubuntu natty main",
333+ "purchase_price": "2.99",
334+ "purchase_date": "2011-09-16 06:37:52",
335+ "state": "Complete",
336+ "failures": [],
337+ "open_id": "https://login.ubuntu.com/+id/ABCDEF",
338+ "application": {
339+ "archive_id": "commercial-ppa-uploaders/photobomb",
340+ "signing_key_id": "1024R/75254D99",
341+ "name": "Photobomb",
342+ "package_name": "photobomb",
343+ "description": "Easy and Social Image Editor\\nPhotobomb give you easy access to images in your social networking feeds, pictures on your computer and peripherals, and pictures on the web, and let\'s you draw, write, crop, combine, and generally have a blast mashing \'em all up. Then you can save off your photobomb, or tweet your creation right back to your social network."
344+ },
345+ "distro_series": {"code_name": "natty", "version": "11.04"}
346+ }
347+]
348+"""
349+# Taken directly from:
350+# https://software-center.ubuntu.com/api/2.0/applications/en/ubuntu/oneiric/i386/
351+AVAILABLE_APPS_JSON = """
352+[
353+ {
354+ "archive_id": "commercial-ppa-uploaders/fluendo-dvd",
355+ "signing_key_id": "1024R/75254D99",
356+ "license": "Proprietary",
357+ "name": "Fluendo DVD Player",
358+ "package_name": "fluendo-dvd",
359+ "support_url": "",
360+ "series": {
361+ "maverick": [
362+ "i386",
363+ "amd64"
364+ ],
365+ "natty": [
366+ "i386",
367+ "amd64"
368+ ],
369+ "oneiric": [
370+ "i386",
371+ "amd64"
372+ ]
373+ },
374+ "price": "24.95",
375+ "demo": null,
376+ "date_published": "2011-12-05 18:43:21.653868",
377+ "status": "Published",
378+ "channel": "For Purchase",
379+ "icon_data": "...",
380+ "department": [
381+ "Sound & Video"
382+ ],
383+ "archive_root": "https://private-ppa.launchpad.net/",
384+ "screenshot_url": "http://software-center.ubuntu.com/site_media/screenshots/2011/05/fluendo-dvd-maverick_.png",
385+ "tos_url": "https://software-center.ubuntu.com/licenses/3/",
386+ "icon_url": "http://software-center.ubuntu.com/site_media/icons/2011/05/fluendo-dvd.png",
387+ "categories": "AudioVideo",
388+ "description": "Play DVD-Videos\\r\\n\\r\\nFluendo DVD Player is a software application specially designed to\\r\\nreproduce DVD on Linux/Unix platforms, which provides end users with\\r\\nhigh quality standards.\\r\\n\\r\\nThe following features are provided:\\r\\n* Full DVD Playback\\r\\n* DVD Menu support\\r\\n* Fullscreen support\\r\\n* Dolby Digital pass-through\\r\\n* Dolby Digital 5.1 output and stereo downmixing support\\r\\n* Resume from last position support\\r\\n* Subtitle support\\r\\n* Audio selection support\\r\\n* Multiple Angles support\\r\\n* Support for encrypted discs\\r\\n* Multiregion, works in all regions\\r\\n* Multiple video deinterlacing algorithms"
389 }
390 ]
391 """
392@@ -39,11 +100,11 @@
393 setattr(self, key, value)
394 self.MimeType = ""
395 self.department = []
396-
397+
398 class MockAvailableForMeList(list):
399
400 def __init__(self):
401- alist = json.loads(AVAILABLE_FOR_ME_JSON)
402+ alist = json.loads(SUBSCRIPTIONS_FOR_ME_JSON)
403 for entry_dict in alist:
404 self.append(MockAvailableForMeItem(entry_dict))
405
406@@ -62,7 +123,8 @@
407 def test_reinstall_purchased_mock(self):
408 # test if the mocks are ok
409 self.assertEqual(len(self.available_to_me), 1)
410- self.assertEqual(self.available_to_me[0].package_name, "hellox")
411+ self.assertEqual(
412+ self.available_to_me[0].application['package_name'], "photobomb")
413
414 def test_reinstall_purchased_xapian(self):
415 db = StoreDatabase("/var/cache/software-center/xapian", self.cache)
416@@ -81,12 +143,167 @@
417 self.assertEqual(len(matches), 1)
418 for m in matches:
419 doc = db.xapiandb.get_document(m.docid)
420- self.assertEqual(doc.get_value(XapianValues.PKGNAME), "hellox")
421- self.assertEqual(doc.get_value(XapianValues.ARCHIVE_SIGNING_KEY_ID), "1024R/0EB12F05")
422+ self.assertEqual(doc.get_value(XapianValues.PKGNAME), "photobomb")
423+ self.assertEqual(
424+ doc.get_value(XapianValues.ARCHIVE_SIGNING_KEY_ID),
425+ "1024R/75254D99")
426 self.assertEqual(doc.get_value(XapianValues.ARCHIVE_DEB_LINE),
427- "deb https://username:randomp3atoken@private-ppa.launchpad.net/mvo/private-test/ubuntu maverick main #Personal access of username to private-test")
428- break # only one match
429-
430+ "deb https://username:random3atoken@"
431+ "private-ppa.launchpad.net/commercial-ppa-uploaders"
432+ "/photobomb/ubuntu natty main")
433+
434+
435+class SoftwareCenterAgentParserTestCase(unittest.TestCase):
436+
437+ def _make_application_parser(self, piston_application=None):
438+ if piston_application is None:
439+ piston_application = PistonResponseObject.from_dict(
440+ json.loads(AVAILABLE_APPS_JSON)[0])
441+ return SoftwareCenterAgentParser(piston_application)
442+
443+ def test_parses_application_from_available_apps(self):
444+ parser = self._make_application_parser()
445+ inverse_map = dict(
446+ (val, key) for key, val in SoftwareCenterAgentParser.MAPPING.items())
447+
448+ # Delete the keys which are not yet provided via the API:
449+ del(inverse_map['video_url'])
450+
451+ for key in inverse_map:
452+ self.assertTrue(parser.has_option_desktop(inverse_map[key]))
453+ self.assertEqual(
454+ getattr(parser.sca_entry, key),
455+ parser.get_desktop(inverse_map[key]))
456+
457+ def test_keys_not_provided_by_api(self):
458+ parser = self._make_application_parser()
459+
460+ self.assertFalse(parser.has_option_desktop('Video-Url'))
461+ self.assertTrue(parser.has_option_desktop('Type'))
462+ self.assertEqual('Application', parser.get_desktop('Type'))
463+
464+ def test_thumbnail_is_screenshot(self):
465+ parser = self._make_application_parser()
466+
467+ self.assertEqual(
468+ "http://software-center.ubuntu.com/site_media/screenshots/"
469+ "2011/05/fluendo-dvd-maverick_.png",
470+ parser.get_desktop('Thumbnail-Url'))
471+
472+ def test_extracts_description(self):
473+ parser = self._make_application_parser()
474+
475+ self.assertEqual("Play DVD-Videos", parser.get_desktop('Comment'))
476+ self.assertEqual(
477+ "Fluendo DVD Player is a software application specially designed "
478+ "to\r\nreproduce DVD on Linux/Unix platforms, which provides end "
479+ "users with\r\nhigh quality standards.\r\n\r\nThe following "
480+ "features are provided:\r\n* Full DVD Playback\r\n* DVD Menu "
481+ "support\r\n* Fullscreen support\r\n* Dolby Digital pass-through"
482+ "\r\n* Dolby Digital 5.1 output and stereo downmixing support\r\n"
483+ "* Resume from last position support\r\n* Subtitle support\r\n"
484+ "* Audio selection support\r\n* Multiple Angles support\r\n"
485+ "* Support for encrypted discs\r\n"
486+ "* Multiregion, works in all regions\r\n"
487+ "* Multiple video deinterlacing algorithms",
488+ parser.get_desktop('Description'))
489+
490+ def test_desktop_categories_uses_department(self):
491+ parser = self._make_application_parser()
492+
493+ self.assertEqual([u'DEPARTMENT:Sound & Video', "AudioVideo"],
494+ parser.get_desktop_categories())
495+
496+ def test_desktop_categories_no_department(self):
497+ piston_app = PistonResponseObject.from_dict(
498+ json.loads(AVAILABLE_APPS_JSON)[0])
499+ del(piston_app.department)
500+ parser = self._make_application_parser(piston_app)
501+
502+ self.assertEqual(["AudioVideo"], parser.get_desktop_categories())
503+
504+ def test_magic_channel(self):
505+ parser = self._make_application_parser()
506+
507+ self.assertEqual(
508+ AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME,
509+ parser.get_desktop('Channel'))
510+
511+
512+class SCAPurchasedApplicationParserTestCase(unittest.TestCase):
513+
514+ def _make_application_parser(self, piston_subscription=None):
515+ if piston_subscription is None:
516+ piston_subscription = PistonResponseObject.from_dict(
517+ json.loads(SUBSCRIPTIONS_FOR_ME_JSON)[0])
518+
519+ return SCAPurchasedApplicationParser(piston_subscription)
520+
521+ def test_get_desktop_subscription(self):
522+ parser = self._make_application_parser()
523+
524+ expected_results = {
525+ "Deb-Line": "deb https://username:random3atoken@"
526+ "private-ppa.launchpad.net/commercial-ppa-uploaders"
527+ "/photobomb/ubuntu natty main",
528+ "Purchased-Date": "2011-09-16 06:37:52",
529+ }
530+ for key in expected_results:
531+ result = parser.get_desktop(key)
532+ self.assertEqual(expected_results[key], result)
533+
534+ def test_get_desktop_application(self):
535+ # The parser passes application attributes through to
536+ # an application parser for handling.
537+ parser = self._make_application_parser()
538+
539+ expected_results = {
540+ "Name": "Photobomb (already purchased)",
541+ "Package": "photobomb",
542+ "Signing-Key-Id": "1024R/75254D99",
543+ "PPA": "commercial-ppa-uploaders/photobomb",
544+ }
545+ for key in expected_results.keys():
546+ result = parser.get_desktop(key)
547+ self.assertEqual(expected_results[key], result)
548+
549+ def test_has_option_desktop_includes_app_keys(self):
550+ # The SCAPurchasedApplicationParser handles application keys also
551+ # (passing them through to the composited application parser).
552+ parser = self._make_application_parser()
553+
554+ for key in ('Name', 'Package', 'Signing-Key-Id', 'PPA'):
555+ self.assertTrue(parser.has_option_desktop(key))
556+ for key in ('Deb-Line', 'Purchased-Date'):
557+ self.assertTrue(parser.has_option_desktop(key),
558+ 'Key: {0} was not an option.'.format(key))
559+
560+ def test_license_key_present(self):
561+ piston_subscription = PistonResponseObject.from_dict(
562+ json.loads(SUBSCRIPTIONS_FOR_ME_JSON)[0])
563+ piston_subscription.license_key = 'abcd'
564+ piston_subscription.license_key_path = '/foo'
565+ parser = self._make_application_parser(piston_subscription)
566+
567+ self.assertTrue(parser.has_option_desktop('License-Key'))
568+ self.assertTrue(parser.has_option_desktop('License-Key-Path'))
569+ self.assertEqual('abcd', parser.get_desktop('License-Key'))
570+ self.assertEqual('/foo', parser.get_desktop('License-Key-Path'))
571+
572+ def test_license_key_not_present(self):
573+ parser = self._make_application_parser()
574+
575+ self.assertFalse(parser.has_option_desktop('License-Key'))
576+ self.assertFalse(parser.has_option_desktop('License-Key-Path'))
577+
578+ def test_magic_channel(self):
579+ parser = self._make_application_parser()
580+
581+ self.assertEqual(
582+ PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME,
583+ parser.get_desktop('Channel'))
584+
585+
586 if __name__ == "__main__":
587 logging.basicConfig(level=logging.DEBUG)
588 unittest.main()