← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:faster-bugs-webservice-tests into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:faster-bugs-webservice-tests into launchpad:master.

Commit message:
Stop using launchpadlib in bugs webservice tests

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/375798

Port the bugs webservice tests to use in-process webservice calls rather than launchpadlib and AppServerLayer.  While the code is a bit longer as a result, it's easier to debug and substantially faster: this change takes the test time for these test suites from 106 seconds to 22 seconds on my laptop.

Similarly, I downgraded a couple of bug subscription filter test suites from LaunchpadFunctionalLayer to DatabaseFunctionalLayer, since they didn't use anything extra from the more sophisticated layer.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:faster-bugs-webservice-tests into launchpad:master.
diff --git a/lib/lp/bugs/browser/tests/test_bugattachment_file_access.py b/lib/lp/bugs/browser/tests/test_bugattachment_file_access.py
index f43e922..30e6cca 100644
--- a/lib/lp/bugs/browser/tests/test_bugattachment_file_access.py
+++ b/lib/lp/bugs/browser/tests/test_bugattachment_file_access.py
@@ -1,15 +1,16 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 import re
-from urlparse import (
+
+import requests
+from six.moves.urllib.parse import (
     parse_qs,
     urlparse,
+    urlunparse,
     )
-
-from lazr.restfulclient.errors import NotFound as RestfulNotFound
 import transaction
 from zope.component import (
     getMultiAdapter,
@@ -17,24 +18,27 @@ from zope.component import (
     )
 from zope.publisher.interfaces import NotFound
 from zope.security.interfaces import Unauthorized
-from zope.security.management import endInteraction
 
 from lp.bugs.browser.bugattachment import BugAttachmentFileNavigation
+from lp.services.config import config
 from lp.services.librarian.interfaces import ILibraryFileAliasWithParent
-from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.interfaces import (
+    ILaunchBag,
+    OAuthPermission,
+    )
 from lp.services.webapp.publisher import RedirectionView
 from lp.services.webapp.servers import LaunchpadTestRequest
 from lp.testing import (
-    launchpadlib_for,
+    api_url,
     login_person,
+    logout,
     TestCaseWithFactory,
-    ws_object,
     )
-from lp.testing.layers import (
-    AppServerLayer,
-    LaunchpadFunctionalLayer,
+from lp.testing.layers import LaunchpadFunctionalLayer
+from lp.testing.pages import (
+    LaunchpadWebServiceCaller,
+    webservice_for_person,
     )
-from lp.testing.pages import LaunchpadWebServiceCaller
 
 
 class TestAccessToBugAttachmentFiles(TestCaseWithFactory):
@@ -119,7 +123,7 @@ class TestAccessToBugAttachmentFiles(TestCaseWithFactory):
 class TestWebserviceAccessToBugAttachmentFiles(TestCaseWithFactory):
     """Tests access to bug attachments via the webservice."""
 
-    layer = AppServerLayer
+    layer = LaunchpadFunctionalLayer
 
     def setUp(self):
         super(TestWebserviceAccessToBugAttachmentFiles, self).setUp()
@@ -127,48 +131,41 @@ class TestWebserviceAccessToBugAttachmentFiles(TestCaseWithFactory):
         getUtility(ILaunchBag).clear()
         login_person(self.bug_owner)
         self.bug = self.factory.makeBug(owner=self.bug_owner)
-        self.bugattachment = self.factory.makeBugAttachment(
+        self.factory.makeBugAttachment(
             bug=self.bug, filename='foo.txt', data='file content')
+        self.bug_url = api_url(self.bug)
 
     def test_anon_access_to_public_bug_attachment(self):
         # Attachments of public bugs can be accessed by anonymous users.
-        #
-        # Need to endInteraction() because launchpadlib_for_anonymous() will
-        # setup a new one.
-        endInteraction()
-        launchpad = launchpadlib_for('test', None, version='devel')
-        ws_bug = ws_object(launchpad, self.bug)
-        ws_bugattachment = ws_bug.attachments[0]
-        self.assertEqual(
-            'file content', ws_bugattachment.data.open().read())
+        logout()
+        webservice = LaunchpadWebServiceCaller(
+            'test', '', default_api_version='devel')
+        ws_bug = self.getWebserviceJSON(webservice, self.bug_url)
+        ws_bug_attachment = self.getWebserviceJSON(
+            webservice, ws_bug['attachments_collection_link'])['entries'][0]
+        response = webservice.get(ws_bug_attachment['data_link'])
+        self.assertEqual(303, response.status)
+        response = requests.get(response.getHeader('Location'))
+        response.raise_for_status()
+        self.assertEqual(b'file content', response.content)
 
     def test_user_access_to_private_bug_attachment(self):
         # Users having access to private bugs can also read attachments
         # of these bugs.
         self.bug.setPrivate(True, self.bug_owner)
         other_user = self.factory.makePerson()
-        launchpad = launchpadlib_for('test', self.bug_owner, version='devel')
-        ws_bug = ws_object(launchpad, self.bug)
-        ws_bugattachment = ws_bug.attachments[0]
-
-        # The attachment contains a link to a HostedBytes resource;
-        # the response to a GET request of this URL is a redirect to a
-        # Librarian URL.  We cannot simply access these Librarian URLs
-        # for restricted Librarian files because the host name used in
-        # the URLs is different for each file, and our test envireonment
-        # does not support wildcard DNS, and because the Launchpadlib
-        # browser automatically follows redirects.
-        # LaunchpadWebServiceCaller, on the other hand, gives us
-        # access to a raw HTTPResonse object.
-        webservice = LaunchpadWebServiceCaller(
-            'launchpad-library', 'salgado-change-anything')
-        response = webservice.get(ws_bugattachment.data._wadl_resource._url)
+        webservice = webservice_for_person(
+            self.bug_owner, permission=OAuthPermission.READ_PRIVATE)
+        ws_bug = self.getWebserviceJSON(webservice, self.bug_url)
+        ws_bug_attachment = self.getWebserviceJSON(
+            webservice, ws_bug['attachments_collection_link'])['entries'][0]
+        response = webservice.get(ws_bug_attachment['data_link'])
         self.assertEqual(303, response.status)
 
         # The Librarian URL has, for our test case, the form
         # "https://NNNN.restricted.launchpad.test:PORT/NNNN/foo.txt?token=...";
         # where NNNN and PORT are integers.
-        parsed_url = urlparse(response.getHeader('location'))
+        parsed_url = urlparse(response.getHeader('Location'))
         self.assertEqual('https', parsed_url.scheme)
         mo = re.search(
             r'^i\d+\.restricted\..+:\d+$', parsed_url.netloc)
@@ -178,10 +175,20 @@ class TestWebserviceAccessToBugAttachmentFiles(TestCaseWithFactory):
         params = parse_qs(parsed_url.query)
         self.assertEqual(['token'], params.keys())
 
+        # Our test environment does not support wildcard DNS.  Work around
+        # this.
+        librarian_netloc = '%s:%d' % (
+            config.librarian.download_host, config.librarian.download_port)
+        url = urlunparse(
+            ('http', librarian_netloc, parsed_url.path, parsed_url.params,
+             parsed_url.query, parsed_url.fragment))
+        response = requests.get(url, headers={'Host': parsed_url.netloc})
+        response.raise_for_status()
+        self.assertEqual(b'file content', response.content)
+
         # If a user which cannot access the private bug itself tries to
-        # to access the attachment, an NotFound error is raised.
-        other_launchpad = launchpadlib_for(
-            'test_unauthenticated', other_user, version='devel')
-        self.assertRaises(
-            RestfulNotFound, other_launchpad._browser.get,
-            ws_bugattachment.data._wadl_resource._url)
+        # to access the attachment, we deny its existence.
+        other_webservice = webservice_for_person(
+            other_user, permission=OAuthPermission.READ_PRIVATE)
+        response = other_webservice.get(ws_bug_attachment['data_link'])
+        self.assertEqual(404, response.status)
diff --git a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
index cd5cacb..1ae3b1e 100644
--- a/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
+++ b/lib/lp/bugs/browser/tests/test_bugsubscriptionfilter.py
@@ -1,18 +1,15 @@
-# Copyright 2010-2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for bug subscription filter browser code."""
 
 __metaclass__ = type
 
-from functools import partial
+import json
 from urlparse import urlparse
 
-from lazr.restfulclient.errors import BadRequest
 from lxml import html
-from storm.exceptions import LostObjectError
 from testtools.matchers import StartsWith
-import transaction
 
 from lp.app.enums import InformationType
 from lp.bugs.browser.structuralsubscription import (
@@ -23,21 +20,19 @@ from lp.bugs.interfaces.bugtask import (
     BugTaskImportance,
     BugTaskStatus,
     )
+from lp.services.webapp.interfaces import OAuthPermission
 from lp.services.webapp.publisher import canonical_url
 from lp.services.webapp.servers import LaunchpadTestRequest
 from lp.testing import (
     anonymous_logged_in,
+    api_url,
     login_person,
     normalize_whitespace,
     person_logged_in,
     TestCaseWithFactory,
-    ws_object,
-    )
-from lp.testing.layers import (
-    AppServerLayer,
-    DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
     )
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
 from lp.testing.views import create_initialized_view
 
 
@@ -53,12 +48,14 @@ class TestBugSubscriptionFilterBase:
                 self.owner, self.owner)
             self.initial_filter = self.subscription.bug_filters.one()
             self.subscription_filter = self.subscription.newBugFilter()
+            self.subscription_url = api_url(self.subscription)
+            self.subscription_filter_url = api_url(self.subscription_filter)
 
 
 class TestBugSubscriptionFilterNavigation(
     TestBugSubscriptionFilterBase, TestCaseWithFactory):
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def test_canonical_url(self):
         url = urlparse(canonical_url(self.subscription_filter))
@@ -80,35 +77,34 @@ class TestBugSubscriptionFilterNavigation(
 class TestBugSubscriptionFilterAPI(
     TestBugSubscriptionFilterBase, TestCaseWithFactory):
 
-    layer = AppServerLayer
+    layer = DatabaseFunctionalLayer
 
     def test_visible_attributes(self):
         # Bug subscription filters are not private objects. All attributes are
         # visible to everyone.
-        transaction.commit()
-        # Create a service for a new person.
-        service = self.factory.makeLaunchpadService()
-        get_ws_object = partial(ws_object, service)
-        ws_subscription = get_ws_object(self.subscription)
-        ws_subscription_filter = get_ws_object(self.subscription_filter)
+        webservice = webservice_for_person(self.factory.makePerson())
+        ws_subscription = self.getWebserviceJSON(
+            webservice, self.subscription_url)
+        ws_subscription_filter = self.getWebserviceJSON(
+            webservice, self.subscription_filter_url)
         self.assertEqual(
-            ws_subscription.self_link,
-            ws_subscription_filter.structural_subscription_link)
+            ws_subscription["self_link"],
+            ws_subscription_filter["structural_subscription_link"])
         self.assertEqual(
             self.subscription_filter.find_all_tags,
-            ws_subscription_filter.find_all_tags)
+            ws_subscription_filter["find_all_tags"])
         self.assertEqual(
             self.subscription_filter.description,
-            ws_subscription_filter.description)
+            ws_subscription_filter["description"])
         self.assertEqual(
             list(self.subscription_filter.statuses),
-            ws_subscription_filter.statuses)
+            ws_subscription_filter["statuses"])
         self.assertEqual(
             list(self.subscription_filter.importances),
-            ws_subscription_filter.importances)
+            ws_subscription_filter["importances"])
         self.assertEqual(
             list(self.subscription_filter.tags),
-            ws_subscription_filter.tags)
+            ws_subscription_filter["tags"])
 
     def test_structural_subscription_cannot_be_modified(self):
         # Bug filters cannot be moved from one structural subscription to
@@ -117,15 +113,14 @@ class TestBugSubscriptionFilterAPI(
         user = self.factory.makePerson(name=u"baz")
         with person_logged_in(self.owner):
             user_subscription = self.structure.addBugSubscription(user, user)
-        transaction.commit()
-        # Create a service for the structure owner.
-        service = self.factory.makeLaunchpadService(self.owner)
-        get_ws_object = partial(ws_object, service)
-        ws_user_subscription = get_ws_object(user_subscription)
-        ws_subscription_filter = get_ws_object(self.subscription_filter)
-        ws_subscription_filter.structural_subscription = ws_user_subscription
-        error = self.assertRaises(BadRequest, ws_subscription_filter.lp_save)
-        self.assertEqual(400, error.response.status)
+            user_subscription_url = api_url(user_subscription)
+        webservice = webservice_for_person(
+            self.owner, permission=OAuthPermission.WRITE_PUBLIC)
+        response = webservice.patch(
+            self.subscription_filter_url, "application/json",
+            json.dumps(
+                {"structural_subscription_link": user_subscription_url}))
+        self.assertEqual(400, response.status)
         self.assertEqual(
             self.subscription,
             self.subscription_filter.structural_subscription)
@@ -134,14 +129,12 @@ class TestBugSubscriptionFilterAPI(
 class TestBugSubscriptionFilterAPIModifications(
     TestBugSubscriptionFilterBase, TestCaseWithFactory):
 
-    layer = AppServerLayer
+    layer = DatabaseFunctionalLayer
 
     def setUp(self):
         super(TestBugSubscriptionFilterAPIModifications, self).setUp()
-        transaction.commit()
-        self.service = self.factory.makeLaunchpadService(self.owner)
-        self.ws_subscription_filter = ws_object(
-            self.service, self.subscription_filter)
+        self.webservice = webservice_for_person(
+            self.owner, permission=OAuthPermission.WRITE_PUBLIC)
 
     def test_modify_tags_fields(self):
         # Two tags-related fields - find_all_tags and tags - can be
@@ -154,11 +147,14 @@ class TestBugSubscriptionFilterAPIModifications(
         self.assertFalse(self.subscription_filter.exclude_any_tags)
         self.assertEqual(set(), self.subscription_filter.tags)
 
-        # Modify, save, and start a new transaction.
-        self.ws_subscription_filter.find_all_tags = True
-        self.ws_subscription_filter.tags = ["foo", "-bar", "*", "-*"]
-        self.ws_subscription_filter.lp_save()
-        transaction.begin()
+        # Apply changes.
+        response = self.webservice.patch(
+            self.subscription_filter_url, "application/json",
+            json.dumps({
+                "find_all_tags": True,
+                "tags": ["foo", "-bar", "*", "-*"],
+                }))
+        self.assertEqual(209, response.status)
 
         # Updated state.
         self.assertTrue(self.subscription_filter.find_all_tags)
@@ -170,13 +166,13 @@ class TestBugSubscriptionFilterAPIModifications(
 
     def test_modify_description(self):
         # The description can be modified.
-        self.assertEqual(
-            None, self.subscription_filter.description)
+        self.assertIsNone(self.subscription_filter.description)
 
-        # Modify, save, and start a new transaction.
-        self.ws_subscription_filter.description = u"It's late."
-        self.ws_subscription_filter.lp_save()
-        transaction.begin()
+        # Apply changes.
+        response = self.webservice.patch(
+            self.subscription_filter_url, "application/json",
+            json.dumps({"description": u"It's late."}))
+        self.assertEqual(209, response.status)
 
         # Updated state.
         self.assertEqual(
@@ -186,10 +182,11 @@ class TestBugSubscriptionFilterAPIModifications(
         # The statuses field can be modified.
         self.assertEqual(set(), self.subscription_filter.statuses)
 
-        # Modify, save, and start a new transaction.
-        self.ws_subscription_filter.statuses = ["New", "Triaged"]
-        self.ws_subscription_filter.lp_save()
-        transaction.begin()
+        # Apply changes.
+        response = self.webservice.patch(
+            self.subscription_filter_url, "application/json",
+            json.dumps({"statuses": ["New", "Triaged"]}))
+        self.assertEqual(209, response.status)
 
         # Updated state.
         self.assertEqual(
@@ -200,10 +197,11 @@ class TestBugSubscriptionFilterAPIModifications(
         # The importances field can be modified.
         self.assertEqual(set(), self.subscription_filter.importances)
 
-        # Modify, save, and start a new transaction.
-        self.ws_subscription_filter.importances = ["Low", "High"]
-        self.ws_subscription_filter.lp_save()
-        transaction.begin()
+        # Apply changes.
+        response = self.webservice.patch(
+            self.subscription_filter_url, "application/json",
+            json.dumps({"importances": ["Low", "High"]}))
+        self.assertEqual(209, response.status)
 
         # Updated state.
         self.assertEqual(
@@ -212,11 +210,13 @@ class TestBugSubscriptionFilterAPIModifications(
 
     def test_delete(self):
         # Subscription filters can be deleted.
-        self.ws_subscription_filter.lp_delete()
-        transaction.begin()
-        self.assertRaises(
-            LostObjectError, getattr, self.subscription_filter,
-            "find_all_tags")
+        self.assertContentEqual(
+            [self.initial_filter, self.subscription_filter],
+            self.subscription.bug_filters)
+        response = self.webservice.delete(self.subscription_filter_url)
+        self.assertEqual(200, response.status)
+        self.assertContentEqual(
+            [self.initial_filter], self.subscription.bug_filters)
 
 
 class TestBugSubscriptionFilterView(
@@ -489,7 +489,7 @@ class TestBugSubscriptionFilterEditView(
 class TestBugSubscriptionFilterAdvancedFeatures(TestCaseWithFactory):
     """A base class for testing advanced structural subscription features."""
 
-    layer = LaunchpadFunctionalLayer
+    layer = DatabaseFunctionalLayer
 
     def setUp(self):
         super(TestBugSubscriptionFilterAdvancedFeatures, self).setUp()
diff --git a/lib/lp/bugs/browser/tests/test_structuralsubscription.py b/lib/lp/bugs/browser/tests/test_structuralsubscription.py
index db341b5..ff2b160 100644
--- a/lib/lp/bugs/browser/tests/test_structuralsubscription.py
+++ b/lib/lp/bugs/browser/tests/test_structuralsubscription.py
@@ -1,11 +1,10 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for structural subscription traversal."""
 
 from urlparse import urlparse
 
-import transaction
 from zope.publisher.interfaces import NotFound
 
 from lp.registry.browser.distribution import DistributionNavigation
@@ -17,19 +16,18 @@ from lp.registry.browser.milestone import MilestoneNavigation
 from lp.registry.browser.product import ProductNavigation
 from lp.registry.browser.productseries import ProductSeriesNavigation
 from lp.registry.browser.project import ProjectNavigation
+from lp.services.webapp.interfaces import OAuthPermission
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import (
+    api_url,
     FakeLaunchpadRequest,
     login,
     logout,
     person_logged_in,
     TestCaseWithFactory,
-    ws_object,
-    )
-from lp.testing.layers import (
-    AppServerLayer,
-    DatabaseFunctionalLayer,
     )
+from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
 from lp.testing.views import create_initialized_view
 
 
@@ -217,7 +215,7 @@ class TestSourcePackageStructuralSubscribersPortletView(
 
 class TestStructuralSubscriptionAPI(TestCaseWithFactory):
 
-    layer = AppServerLayer
+    layer = DatabaseFunctionalLayer
 
     def setUp(self):
         super(TestStructuralSubscriptionAPI, self).setUp()
@@ -228,40 +226,59 @@ class TestStructuralSubscriptionAPI(TestCaseWithFactory):
             self.subscription = self.structure.addBugSubscription(
                 self.owner, self.owner)
             self.initial_filter = self.subscription.bug_filters[0]
-        transaction.commit()
-        self.service = self.factory.makeLaunchpadService(self.owner)
-        self.ws_subscription = ws_object(self.service, self.subscription)
-        self.ws_subscription_filter = ws_object(
-            self.service, self.initial_filter)
+            self.subscription_url = api_url(self.subscription)
+            self.initial_filter_url = api_url(self.initial_filter)
+        self.webservice = webservice_for_person(
+            self.owner, permission=OAuthPermission.WRITE_PUBLIC)
 
     def test_newBugFilter(self):
         # New bug subscription filters can be created with newBugFilter().
-        ws_subscription_filter = self.ws_subscription.newBugFilter()
+        ws_subscription = self.getWebserviceJSON(
+            self.webservice, self.subscription_url)
+        response = self.webservice.named_post(
+            self.subscription_url, "newBugFilter")
+        self.assertEqual(201, response.status)
+        ws_subscription_filter = self.getWebserviceJSON(
+            self.webservice, response.getHeader("Location"))
         self.assertEqual(
             "bug_subscription_filter",
-            urlparse(ws_subscription_filter.resource_type_link).fragment)
+            urlparse(ws_subscription_filter["resource_type_link"]).fragment)
         self.assertEqual(
-            ws_subscription_filter.structural_subscription.self_link,
-            self.ws_subscription.self_link)
+            ws_subscription["self_link"],
+            ws_subscription_filter["structural_subscription_link"])
 
     def test_bug_filters(self):
         # The bug_filters property is a collection of IBugSubscriptionFilter
         # instances previously created by newBugFilter().
-        bug_filter_links = lambda: set(
-            bug_filter.self_link for bug_filter in (
-                self.ws_subscription.bug_filters))
-        initial_filter_link = self.ws_subscription_filter.self_link
+        ws_subscription = self.getWebserviceJSON(
+            self.webservice, self.subscription_url)
+        ws_initial_filter = self.getWebserviceJSON(
+            self.webservice, self.initial_filter_url)
+
+        def bug_filter_links():
+            ws_bug_filters = self.getWebserviceJSON(
+                self.webservice,
+                ws_subscription["bug_filters_collection_link"])
+            return {entry["self_link"] for entry in ws_bug_filters["entries"]}
+
+        initial_filter_link = ws_initial_filter["self_link"]
         self.assertContentEqual(
             [initial_filter_link], bug_filter_links())
         # A new filter appears in the bug_filters collection.
-        ws_subscription_filter1 = self.ws_subscription.newBugFilter()
+        response = self.webservice.named_post(
+            self.subscription_url, "newBugFilter")
+        self.assertEqual(201, response.status)
+        ws_subscription_filter1_link = response.getHeader("Location")
         self.assertContentEqual(
-            [ws_subscription_filter1.self_link, initial_filter_link],
+            [ws_subscription_filter1_link, initial_filter_link],
             bug_filter_links())
         # A second new filter also appears in the bug_filters collection.
-        ws_subscription_filter2 = self.ws_subscription.newBugFilter()
+        response = self.webservice.named_post(
+            self.subscription_url, "newBugFilter")
+        self.assertEqual(201, response.status)
+        ws_subscription_filter2_link = response.getHeader("Location")
         self.assertContentEqual(
-            [ws_subscription_filter1.self_link,
-             ws_subscription_filter2.self_link,
+            [ws_subscription_filter1_link,
+             ws_subscription_filter2_link,
              initial_filter_link],
             bug_filter_links())
diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
index c2874ba..7466f7e 100644
--- a/lib/lp/bugs/model/tests/test_bugtask.py
+++ b/lib/lp/bugs/model/tests/test_bugtask.py
@@ -10,7 +10,6 @@ import subprocess
 import unittest
 
 from lazr.lifecycle.snapshot import Snapshot
-from lazr.restfulclient.errors import Unauthorized
 from storm.store import Store
 from testtools.matchers import Equals
 from testtools.testcase import ExpectedException
@@ -86,12 +85,16 @@ from lp.services.log.logger import DevNullLogger
 from lp.services.propertycache import get_property_cache
 from lp.services.searchbuilder import any
 from lp.services.webapp.authorization import check_permission
-from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.interfaces import (
+    ILaunchBag,
+    OAuthPermission,
+    )
 from lp.services.webapp.snapshot import notify_modified
 from lp.soyuz.interfaces.archive import ArchivePurpose
 from lp.testing import (
     admin_logged_in,
     ANONYMOUS,
+    api_url,
     EventRecorder,
     feature_flags,
     login,
@@ -103,15 +106,14 @@ from lp.testing import (
     StormStatementRecorder,
     TestCase,
     TestCaseWithFactory,
-    ws_object,
     )
 from lp.testing.fakemethod import FakeMethod
 from lp.testing.layers import (
-    AppServerLayer,
     CeleryJobLayer,
     DatabaseFunctionalLayer,
     )
 from lp.testing.matchers import HasQueryCount
+from lp.testing.pages import webservice_for_person
 
 
 BugData = namedtuple("BugData", ['owner', 'distro', 'distro_release',
@@ -2953,29 +2955,32 @@ class TestValidateNewTarget(TestCaseWithFactory, ValidateTargetMixin):
 class TestWebservice(TestCaseWithFactory):
     """Tests for the webservice."""
 
-    layer = AppServerLayer
+    layer = DatabaseFunctionalLayer
 
     def test_delete_bugtask(self):
         """Test that a bugtask can be deleted."""
         owner = self.factory.makePerson()
         some_person = self.factory.makePerson()
-        db_bug = self.factory.makeBug()
-        db_bugtask = self.factory.makeBugTask(bug=db_bug, owner=owner)
-        transaction.commit()
-        logout()
+        bug = self.factory.makeBug()
+        bugtask = self.factory.makeBugTask(bug=bug, owner=owner)
+        bugtask_url = api_url(bugtask)
 
         # It will fail for an unauthorised user.
-        launchpad = self.factory.makeLaunchpadService(some_person)
-        bugtask = ws_object(launchpad, db_bugtask)
-        self.assertRaises(Unauthorized, bugtask.lp_delete)
+        webservice = webservice_for_person(
+            some_person, permission=OAuthPermission.WRITE_PUBLIC,
+            default_api_version="devel")
+        response = webservice.delete(bugtask_url)
+        self.assertEqual(401, response.status)
+
+        webservice = webservice_for_person(
+            owner, permission=OAuthPermission.WRITE_PUBLIC,
+            default_api_version="devel")
+        response = webservice.delete(bugtask_url)
+        self.assertEqual(200, response.status)
 
-        launchpad = self.factory.makeLaunchpadService(owner)
-        bugtask = ws_object(launchpad, db_bugtask)
-        bugtask.lp_delete()
-        transaction.commit()
         # Check the delete really worked.
-        with person_logged_in(removeSecurityProxy(db_bug).owner):
-            self.assertEqual([db_bug.default_bugtask], db_bug.bugtasks)
+        with person_logged_in(removeSecurityProxy(bug).owner):
+            self.assertEqual([bug.default_bugtask], bug.bugtasks)
 
 
 class TestBugTaskUserHasBugSupervisorPrivileges(TestCaseWithFactory):
diff --git a/lib/lp/bugs/tests/test_bug_messages_webservice.py b/lib/lp/bugs/tests/test_bug_messages_webservice.py
index feec1a7..5faae0e 100644
--- a/lib/lp/bugs/tests/test_bug_messages_webservice.py
+++ b/lib/lp/bugs/tests/test_bug_messages_webservice.py
@@ -1,4 +1,4 @@
-# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Webservice unit tests related to Launchpad Bug messages."""
@@ -24,36 +24,43 @@ from lp.testing import (
     logout,
     person_logged_in,
     TestCaseWithFactory,
-    WebServiceTestCase,
     )
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
     )
-from lp.testing.pages import LaunchpadWebServiceCaller
+from lp.testing.pages import (
+    LaunchpadWebServiceCaller,
+    webservice_for_person,
+    )
 
 
-class TestMessageTraversal(WebServiceTestCase):
+class TestMessageTraversal(TestCaseWithFactory):
     """Tests safe traversal of bugs.
 
     See bug 607438."""
 
+    layer = LaunchpadFunctionalLayer
+
     def test_message_with_attachments(self):
-        bugowner = self.factory.makePerson()
-        bug = self.factory.makeBug(owner=bugowner)
+        bug = self.factory.makeBug()
         # Traversal over bug messages attachments has no errors.
         expected_messages = []
-        with person_logged_in(bugowner):
+        with person_logged_in(bug.owner):
             for i in range(3):
                 att = self.factory.makeBugAttachment(bug)
                 expected_messages.append(att.message.subject)
-
-        lp_user = self.factory.makePerson()
-        lp_bug = self.wsObject(bug, lp_user)
-
-        attachments = lp_bug.attachments
-        messages = [a.message.subject for a in attachments
-            if a.message is not None]
+        bug_url = api_url(bug)
+
+        webservice = webservice_for_person(self.factory.makePerson())
+        ws_bug = self.getWebserviceJSON(webservice, bug_url)
+        ws_bug_attachments = self.getWebserviceJSON(
+            webservice, ws_bug['attachments_collection_link'])
+        messages = [
+            self.getWebserviceJSON(
+                webservice, attachment['message_link'])['subject']
+            for attachment in ws_bug_attachments['entries']
+            if attachment['message_link'] is not None]
         self.assertContentEqual(
             messages,
             expected_messages)
@@ -67,15 +74,21 @@ class TestMessageTraversal(WebServiceTestCase):
         message_2 = self.factory.makeMessage()
         message_2.parent = message_1
         bug = self.factory.makeBug()
-        bug.linkMessage(message_2)
+        with person_logged_in(bug.owner):
+            bug.linkMessage(message_2)
+        bug_url = api_url(bug)
+        message_2_url = api_url(message_2)
         user = self.factory.makePerson()
-        lp_bug = self.wsObject(bug, user)
-        for lp_message in lp_bug.messages:
+        webservice = webservice_for_person(user)
+        ws_bug = self.getWebserviceJSON(webservice, bug_url)
+        ws_bug_messages = self.getWebserviceJSON(
+            webservice, ws_bug['messages_collection_link'])
+        for ws_message in ws_bug_messages['entries']:
             # An IIndexedMessage's representation.
-            self.assertIs(None, lp_message.parent)
+            self.assertIsNone(ws_message['parent_link'])
         # An IMessage's representation.
-        lp_message = self.wsObject(message_2, user)
-        self.assertIs(None, lp_message.parent)
+        ws_message = self.getWebserviceJSON(webservice, message_2_url)
+        self.assertIsNone(ws_message['parent_link'])
 
 
 class TestBugMessage(TestCaseWithFactory):
diff --git a/lib/lp/testing/__init__.py b/lib/lp/testing/__init__.py
index 6a0d9b2..747930d 100644
--- a/lib/lp/testing/__init__.py
+++ b/lib/lp/testing/__init__.py
@@ -827,6 +827,12 @@ class TestCase(testtools.TestCase, fixtures.TestWithFixtures):
                 "\n\n".join(str(n) for n in notifications)))
         return notifications
 
+    def getWebserviceJSON(self, webservice, url):
+        """Get the JSON representation of a webservice object given its URL."""
+        response = webservice.get(url)
+        self.assertEqual(200, response.status)
+        return response.jsonBody()
+
 
 class TestCaseWithFactory(TestCase):