Permalink
Browse files

Merge pull request #23 from klrmn/env_api

CRUD APIs for Profile, Category, and Element
Looks good to me!
  • Loading branch information...
2 parents c98da16 + 483d5c4 commit 2893b95b60047f9f318132e760b986122255969e @camd camd committed Mar 13, 2013
View
56 docs/userguide/api/core.rst
@@ -5,9 +5,6 @@ Product
-------
.. http:get:: /api/v1/product
-.. http:post:: /api/v1/product
-.. http:delete:: /api/v1/product/<id>
-.. http:put:: /api/v1/product/<id>
Filtering
^^^^^^^^^
@@ -18,14 +15,39 @@ Filtering
GET /api/v1/product/?format=json&name=Firefox
+.. http:get:: /api/v1/product/<id>
+.. http:post:: /api/v1/product
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :name: A string Product name.
+ :productversions: A list of at least one Product Version.
+
+Optional Fields
+^^^^^^^^^^^^^^^
+
+ :description: A string description.
+
+.. http:delete:: /api/v1/product/<id>
+
+.. note::
+
+ Deleting a Product will delete all of it's child objects.
+
+.. http:put:: /api/v1/product/<id>
+
+.. note::
+
+ ProductVersions are displayed in the GET results. They may be added to
+ or changed by a POST request, but a POST to Product will not delete
+ any ProductVersion.
+
Product Version
---------------
.. http:get:: /api/v1/productversion
-.. http:post:: /api/v1/productversion
-.. http:delete:: /api/v1/productversion/<id>
-.. http:put:: /api/v1/productversion/<id>
Filtering
^^^^^^^^^
@@ -42,3 +64,25 @@ Filtering
GET /api/v1/productversion/?format=json&version=10
GET /api/v1/productversion/?format=json&product__name=Firefox
+
+.. http:get:: /api/v1/productversion/<id>
+
+.. http:post:: /api/v1/productversion
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :version: A string ProductVersion name.
+ :product: A resource uri of the parent Product.
+
+Optional Fields
+^^^^^^^^^^^^^^^
+
+ :codename: A string codename.
+
+.. http:delete:: /api/v1/productversion/<id>
+.. http:put:: /api/v1/productversion/<id>
+
+.. note::
+
+ The Product of an existing ProductVersion may not be changed.
View
71 docs/userguide/api/environments.rst
@@ -1,6 +1,77 @@
Environment API
===============
+Profile
+-------
+
+.. http:get:: /api/v1/profile
+
+Filtering
+^^^^^^^^^
+
+ :name: The ``name`` of the Profile to filter on.
+
+.. http:get:: /api/v1/profile/<id>
+.. http:post:: /api/v1/profile
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :name: A string Profile name.
+
+.. http:delete:: /api/v1/profile/<id>
+.. http:put:: /api/v1/profile/<id>
+
+Category
+--------
+
+.. http:get:: /api/v1/category
+
+Filtering
+^^^^^^^^^
+
+ :name: The ``name`` of the Category to filter on.
+
+.. http:get:: /api/v1/category/<id>
+.. http:post:: /api/v1/category
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :name: A string Category name.
+
+.. http:delete:: /api/v1/category/<id>
+.. http:put:: /api/v1/category/<id>
+
+Element
+-------
+
+.. http:get:: /api/v1/element/
+
+Filtering
+^^^^^^^^^
+
+ :name: The ``name`` of the Element to filter on.
+ :category: The ``id`` of the Category to filter on.
+ :category__name: The ``name`` of the Category to filter on.
+
+.. http:get:: /api/v1/element/<id>
+.. http:post:: /api/v1/element
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :name: A string Element name.
+ :category: A resource uri to the parent Category.
+
+.. http:delete:: /api/v1/element/<id>
+.. http:put:: /api/v1/element/<id>
+
+
+.. note::
+
+ The Category of an existing Element may not be changed.
+
Environment
-----------
View
35 docs/userguide/api/tags.rst
@@ -1,23 +1,42 @@
Tags API
-=============
+========
Tag
---------
+---
.. http:get:: /api/v1/tag
-.. http:post:: /api/v1/suite
-.. http:delete:: /api/v1/suite/<id>
-.. http:put:: /api/v1/suite/<id>
Filtering
^^^^^^^^^
- :name: The name of the tag.
- :product: The id of the product.
- :product__name: The name of the product
+ :name: The Tag ``name`` to filter on.
+ :product: The Product ``id`` to filter on.
+ :product__name: The Product ``name`` to filter on.
**Example request**:
.. sourcecode:: http
GET /api/v1/tag/?format=json
+
+.. http:get:: /api/v1/tag/<id>
+.. http:post:: /api/v1/suite
+
+Required Fields
+^^^^^^^^^^^^^^^
+
+ :name: A string name for the Tag.
+ :product: A resource uri to a Product.
+
+Optional Fields
+^^^^^^^^^^^^^^^
+
+ :description: A string description for the Tag.
+
+.. http:delete:: /api/v1/suite/<id>
+.. http:put:: /api/v1/suite/<id>
+
+.. note::
+
+ The Tag's Product may not be changed unless the tag is not in use, the
+ product is being set to None, or the product matches the existing cases."
View
29 moztrap/model/core/api.py
@@ -59,6 +59,35 @@ def model(self):
return ProductVersion
+ @property
+ def read_create_fields(self):
+ """product is read-only"""
+ return ["product"]
+
+
+ def obj_update(self, bundle, request=None, **kwargs):
+ """Avoid concurrency error caused by the setting of latest_version"""
+ bundle = self.check_read_create(bundle)
+
+ try:
+ # use grandparent rather than parent
+ bundle = super(MTResource, self).obj_update(
+ bundle=bundle, request=request, **kwargs)
+
+ # update the cc_version
+ bundle.obj.cc_version = self.model.objects.get(
+ id=bundle.obj.id).cc_version
+
+ # specify the user
+ bundle.obj.save(user=request.user)
+
+ except Exception: # pragma: no cover
+ logger.exception("error updating %s", bundle) # pragma: no cover
+ raise # pragma: no cover
+
+ return bundle
+
+
class ProductResource(MTResource):
"""
View
87 moztrap/model/environments/api.py
@@ -1,28 +1,91 @@
from tastypie import fields
-from tastypie.resources import ModelResource, ALL
+from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
+from ..mtapi import MTResource, MTAuthorization
-from .models import Environment, Element, Category
+from .models import Profile, Environment, Element, Category
-class CategoryResource(ModelResource):
- """Return a list of environment categories."""
+class EnvironmentAuthorization(MTAuthorization):
+ """Atypically named permission."""
- class Meta:
+ @property
+ def permission(self):
+ """This permission should be checked by is_authorized."""
+ return "environments.manage_environments"
+
+
+
+class ProfileResource(MTResource):
+ """Create, Read, Update, and Delete capabilities for Profile."""
+
+ class Meta(MTResource.Meta):
+ queryset = Profile.objects.all()
+ fields = ["id", "name"]
+ authorization = EnvironmentAuthorization()
+ ordering = ["id", "name"]
+ filtering = {
+ "name": ALL,
+ }
+
+ @property
+ def model(self):
+ """Model class related to this resource."""
+ return Profile
+
+
+
+class CategoryResource(MTResource):
+ """Create, Read, Update and Delete capabilities for Category."""
+
+ elements = fields.ToManyField(
+ "moztrap.model.environments.api.ElementResource",
+ "elements",
+ full=True,
+ readonly=True
+ )
+
+ class Meta(MTResource.Meta):
queryset = Category.objects.all()
- list_allowed_methods = ['get']
fields = ["id", "name"]
+ authorization = EnvironmentAuthorization()
+ ordering = ["id", "name"]
+ filtering = {
+ "name": ALL,
+ }
+ @property
+ def model(self):
+ """Model class related to this resource."""
+ return Category
-class ElementResource(ModelResource):
- """Return a list of environment elements."""
- category = fields.ForeignKey(CategoryResource, "category", full=True)
- class Meta:
+class ElementResource(MTResource):
+ """Create, Read, Update and Delete capabilities for Element."""
+
+ category = fields.ForeignKey(CategoryResource, "category")
+
+ class Meta(MTResource.Meta):
queryset = Element.objects.all()
- list_allowed_methods = ['get']
- fields = ["id", "name"]
+ fields = ["id", "name", "category"]
+ authorization = EnvironmentAuthorization()
+ filtering = {
+ "category": ALL_WITH_RELATIONS,
+ "name": ALL,
+ }
+ ordering = ["id", "name"]
+
+
+ @property
+ def model(self):
+ """Model class related to this resource."""
+ return Element
+
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["category"]
View
185 moztrap/model/library/api.py
@@ -1,6 +1,5 @@
from tastypie import http, fields
-from tastypie.bundle import Bundle
-from tastypie.exceptions import BadRequest, ImmediateHttpResponse
+from tastypie.exceptions import ImmediateHttpResponse
from tastypie.resources import ModelResource, ALL, ALL_WITH_RELATIONS
from ..core.api import (ProductVersionResource, ProductResource,
@@ -54,12 +53,18 @@ class Meta(MTResource.Meta):
}
ordering = ['name', 'product__id', 'id']
+
@property
def model(self):
"""Model class related to this resource."""
return Suite
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["product"]
+
class CaseResource(MTResource):
"""
@@ -85,20 +90,10 @@ def model(self):
return Case
- def hydrate_product(self, bundle):
- """product is read-only on PUT"""
- if bundle.request.META['REQUEST_METHOD'] == 'PUT':
- case = self.get_via_uri(bundle.request.path)
- prod_id = self._id_from_uri(bundle.data['product'])
- if str(case.product.id) != prod_id:
- error_msg = "product of an existing case may not be changed."
- logger.error(
- "\n".join([error_msg, "old: %s, new: %s"]),
- case.product.id, prod_id)
- raise ImmediateHttpResponse(
- response=http.HttpBadRequest(error_msg))
-
- return bundle
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["product"]
@@ -127,27 +122,10 @@ def model(self):
return CaseStep
- def hydrate_caseversion(self, bundle):
- """caseversion is read-only on PUT
- """
- if 'caseversion' not in bundle.data.keys():
- return bundle
-
- # edit (PUT)
- if bundle.request.META['REQUEST_METHOD'] == 'PUT':
- cs = self.get_via_uri(bundle.request.path)
- cv_id = self._id_from_uri(bundle.data['caseversion'])
- if str(cs.caseversion.id) != cv_id:
- error_message = str(
- "caseversion of an existing casestep " +
- "may not be changed.")
- logger.error(
- "\n".join([error_message, "old: %s, new: %s"]),
- cs.caseversion.id, cv_id)
- raise ImmediateHttpResponse(
- response=http.HttpBadRequest(error_message))
-
- return bundle
+ @property
+ def read_create_fields(self):
+ """caseversion is read-only"""
+ return ["caseversion"]
@@ -174,64 +152,31 @@ class Meta(MTResource.Meta):
def model(self):
return SuiteCase
+
+ @property
+ def read_create_fields(self):
+ """case and suite are read-only"""
+ return ["suite", "case"]
+
+
def hydrate_case(self, bundle):
"""case is read-only on PUT
case.product must match suite.product on CREATE
"""
- # edit (PUT)
- if bundle.request.META['REQUEST_METHOD'] == 'PUT':
- if 'case' not in bundle.data.keys():
- return bundle
- sc = self.get_via_uri(bundle.request.path)
- case_id = self._id_from_uri(bundle.data['case'])
- if str(sc.case.id) != case_id:
- error_message = str(
- "case of an existing suitecase " +
- "may not be changed.")
- logger.error(
- "\n".join([error_message, "old: %s, new: %s"]),
- sc.case.id, case_id)
- raise ImmediateHttpResponse(
- response=http.HttpBadRequest(error_message))
-
- return bundle
-
# CREATE
- case_id = self._id_from_uri(bundle.data['case'])
- case = Case.objects.get(id=case_id)
- suite_id = self._id_from_uri(bundle.data['suite'])
- suite = Suite.objects.get(id=suite_id)
- if case.product.id != suite.product.id:
- error_message = str(
- "case's product must match suite's product."
- )
- logger.error(
- "\n".join([error_message, "case prod: %s, suite prod: %s"]),
- case.product.id, suite.product.id)
- raise ImmediateHttpResponse(
- response=http.HttpBadRequest(error_message))
-
- return bundle
-
-
- def hydrate_suite(self, bundle):
- """suite is read-only on PUT
- """
-
- # edit (PUT)
- if bundle.request.META['REQUEST_METHOD'] == 'PUT':
- if 'suite' not in bundle.data.keys():
- return bundle
- sc = self.get_via_uri(bundle.request.path)
+ if bundle.request.META['REQUEST_METHOD'] == 'POST':
+ case_id = self._id_from_uri(bundle.data['case'])
+ case = Case.objects.get(id=case_id)
suite_id = self._id_from_uri(bundle.data['suite'])
- if str(sc.suite.id) != suite_id:
+ suite = Suite.objects.get(id=suite_id)
+ if case.product.id != suite.product.id:
error_message = str(
- "suite of an existing suitecase " +
- "may not be changed.")
+ "case's product must match suite's product."
+ )
logger.error(
- "\n".join([error_message, "old: %s, new: %s"]),
- sc.suite.id, suite_id)
+ "\n".join([error_message, "case prod: %s, suite prod: %s"]),
+ case.product.id, suite.product.id)
raise ImmediateHttpResponse(
response=http.HttpBadRequest(error_message))
@@ -274,63 +219,29 @@ def model(self):
return CaseVersion
- def hydrate_productversion(self, bundle):
- """productversion is read-only on PUT
- case.product must match productversion.product on CREATE
- """
- if 'productversion' not in bundle.data.keys():
- return bundle
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["case", "productversion"]
- # edit (PUT)
- if bundle.request.META['REQUEST_METHOD'] == 'PUT':
- cv = self.get_via_uri(bundle.request.path)
- pv_id = self._id_from_uri(bundle.data['productversion'])
- if str(cv.productversion.id) != pv_id:
- error_message = str(
- "productversion of an existing caseversion " +
- "may not be changed.")
- logger.error(
- "\n".join([error_message, "old: %s, new: %s"]),
- cv.productversion.id, pv_id)
- raise ImmediateHttpResponse(
- response=http.HttpBadRequest(error_message))
- return bundle
+ def hydrate_productversion(self, bundle):
+ """case.product must match productversion.product on CREATE"""
# create
- pv_id = self._id_from_uri(bundle.data['productversion'])
- pv = ProductVersion.objects.get(id=pv_id)
- case_id = self._id_from_uri(bundle.data['case'])
- case = Case.objects.get(id=case_id)
- if not case.product.id == pv.product.id:
- message = str("productversion must match case's product")
- logger.error("\n".join([message,
- "productversion product id: %s case product id: %s"], ),
- pv.product.id,
- case.product.id)
- raise ImmediateHttpResponse(
- response=http.HttpBadRequest(message))
-
- return bundle
-
-
- def hydrate_case(self, bundle):
- """case is a primary key and as such is not editable."""
-
- if bundle.request.META['REQUEST_METHOD'] == 'PUT':
- if 'case' not in bundle.data.keys():
- return bundle
-
- cv = self.get_via_uri(bundle.request.path)
+ if bundle.request.META['REQUEST_METHOD'] == 'POST':
+ pv_id = self._id_from_uri(bundle.data['productversion'])
+ pv = ProductVersion.objects.get(id=pv_id)
case_id = self._id_from_uri(bundle.data['case'])
- if str(cv.case.id) != case_id:
- error_message = str(
- "case of an existing caseversion may not be changed.")
- logger.error(
- "\n".join([error_message, "old: %s, new: %s"]),
- cv.case.id, case_id)
+ case = Case.objects.get(id=case_id)
+ if not case.product.id == pv.product.id:
+ message = str("productversion must match case's product")
+ logger.error("\n".join([message,
+ "productversion product id: %s case product id: %s"], ),
+ pv.product.id,
+ case.product.id)
raise ImmediateHttpResponse(
- response=http.HttpBadRequest(error_message))
+ response=http.HttpBadRequest(message))
return bundle
View
35 moztrap/model/mtapi.py
@@ -1,6 +1,8 @@
-from tastypie.resources import ModelResource
+from tastypie import http
from tastypie.authentication import ApiKeyAuthentication
from tastypie.authorization import Authorization
+from tastypie.exceptions import ImmediateHttpResponse
+from tastypie.resources import ModelResource
from .core.models import ApiKey
@@ -109,6 +111,36 @@ def model(self):
raise NotImplementedError # pragma: no cover
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return []
+
+
+ def check_read_create(self, bundle):
+ """Verify that request isn't trying to change a read-create field."""
+
+ obj = self.get_via_uri(bundle.request.path)
+ for fk in self.read_create_fields:
+
+ if fk not in bundle.data:
+ continue
+
+ new_fk_id = self._id_from_uri(bundle.data[fk])
+ old_fk_id = str(getattr(obj, fk).id)
+ if new_fk_id != old_fk_id:
+ error_message = str(
+ "%s of an existing %s " % (fk, self._meta.resource_name) +
+ "may not be changed.")
+ logger.error(
+ "\n".join([error_message, "old: %s, new: %s"]),
+ old_fk_id, new_fk_id)
+ raise ImmediateHttpResponse(
+ response=http.HttpBadRequest(error_message))
+
+ return bundle
+
+
def obj_create(self, bundle, request=None, **kwargs):
"""Set the created_by field for the object to the request's user"""
# this try/except logging is more helpful than 500 / 404 errors on
@@ -128,6 +160,7 @@ def obj_update(self, bundle, request=None, **kwargs):
"""Set the modified_by field for the object to the request's user"""
# this try/except logging is more helpful than 500 / 404 errors on the
# client side
+ bundle = self.check_read_create(bundle)
try:
bundle = super(MTResource, self).obj_update(
bundle=bundle, request=request, **kwargs)
View
3 moztrap/model/tags/api.py
@@ -39,6 +39,9 @@ def model(self):
return Tag
+ # do not put read_create_fields here, as product is a special
+ # case that may be changed some times but not others
+
def obj_update(self, bundle, request=None, **kwargs):
"""Lots of rules for modifying product for tags."""
tag = self.get_via_uri(bundle.request.path, request)
View
1 moztrap/view/api/urls.py
@@ -23,6 +23,7 @@
v1_api.register(library.CaseSelectionResource())
v1_api.register(library.SuiteCaseResource())
v1_api.register(library.CaseVersionSelectionResource())
+v1_api.register(environments.ProfileResource())
v1_api.register(environments.EnvironmentResource())
v1_api.register(environments.ElementResource())
v1_api.register(environments.CategoryResource())
View
78 tests/case/api/crud.py
@@ -25,6 +25,10 @@ class ApiCrudCases(ApiTestCase):
If any of these properties / methods are called on a child class without
having implemented them, a NotImplementedError will be thrown.
+ The following methods have default behavior, but may be extended:
+ - manipulate_edit_data (method)
+ - read_create_fields (property)
+
Child classes are may extend setUp() to provide required fixtures.
The test methods provided by this class are:
@@ -33,6 +37,8 @@ class ApiCrudCases(ApiTestCase):
- test_read_list()
- test_read_detail()
- test_update_detail()
+ - test_change_fk_should_error()
+ - test_update_without_fk()
- test_update_list_forbidden()
- test_update_fails_without_creds()
- test_delete_detail_perminant()
@@ -171,12 +177,22 @@ def backend_meta_data(self, backend_obj):
def manipulate_edit_data(self, fixture, fields):
- """may be used to replace items in the fields dicts with the values
- from the fixture, so as not to disturb read-only fields."""
+ """replace the data for the foreign keys with the current values."""
+
+ for fk in self.read_create_fields:
+ fields[fk] = unicode(
+ self.get_detail_url(fk, str(getattr(fixture, fk).id)))
+
return fields
@property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return []
+
+
+ @property
def datetime(self):
"""May be used to provide mostly-unique strings"""
return datetime.utcnow().isoformat()
@@ -418,6 +434,64 @@ def test_update_detail(self):
meta_before['modified_on'], meta_after['modified_on'])
+ def test_change_fk_should_error(self):
+ """Trying to change a read-only foreign key should result in a 400 error."""
+
+ if self.is_abstract_class:
+ return
+
+ for fk in self.read_create_fields:
+ mozlogger.info('test_change_fk_should_error %s' % fk)
+
+ # create fixture
+ fixture1 = self.factory
+ obj_id = str(fixture1.id)
+ fields = self.new_object_data
+
+ # make sure only the fk under test is inappropriate
+ if len(self.read_create_fields) > 1:
+ fk_value = fields[fk]
+ fields = self.manipulate_edit_data(fixture1, fields)
+ fields[fk] = fk_value
+
+ # do put
+ res = self.put(
+ self.get_detail_url(self.resource_name, obj_id),
+ params=self.credentials,
+ data=fields,
+ status=400,
+ )
+
+ assert res.text == str(
+ "%s of an existing %s " % (fk, self.resource_name) +
+ "may not be changed.")
+
+
+
+ def test_update_without_fk(self):
+ """fk's cannot be changed, so they are not required on edit."""
+
+ if self.is_abstract_class:
+ return
+
+ # fixtures
+ fixture1 = self.factory
+
+ for fk in self.read_create_fields:
+ mozlogger.info('test_update_without_fk %s' % fk)
+
+ fields = self.backend_data(fixture1)
+ fields = self.manipulate_edit_data(fixture1, fields)
+ fields.pop(fk)
+
+ # do put
+ res = self.put(
+ self.get_detail_url(self.resource_name, fixture1.id),
+ params=self.credentials,
+ data=fields,
+ )
+
+
def test_update_list_forbidden(self):
"""Attempts to PUT to the list uri.
Verifies that the request is rejected with a 405 error.
View
10 tests/model/core/api/test_productversion_env_resource.py
@@ -53,14 +53,8 @@ def test_productversionenvironments_list(self):
u"codename": unicode(pv.codename),
u'environments': [{
u'elements': [{
- u'category': {
- u'id': unicode(category.id),
- u'name': u'OS',
- u'resource_uri': unicode(self.get_detail_url(
- "category",
- category.id,
- )),
- },
+ u'category': unicode(
+ self.get_detail_url("category", category.id)),
u'id': unicode(element.id),
u'name': u'OS X',
u'resource_uri': unicode(self.get_detail_url(
View
5 tests/model/core/api/test_productversion_resource.py
@@ -77,4 +77,9 @@ def backend_data(self, backend_obj):
}
+ @property
+ def read_create_fields(self):
+ """product is read-only"""
+ return ["product"]
+
# additional test cases, if any
View
81 tests/model/environments/api/test_category_resource.py
@@ -3,51 +3,70 @@
"""
-from tests import case
+from tests.case.api.crud import ApiCrudCases
-class CategoryResourceTest(case.api.ApiTestCase):
+class CategoryResourceTest(ApiCrudCases):
@property
def factory(self):
"""The model factory for this object."""
- return self.F.CategoryFactory
+ return self.F.CategoryFactory()
@property
def resource_name(self):
+ """The resource name for this object."""
return "category"
- def test_category_list(self):
- """Get a list of existing categories"""
- category = self.factory.create(name="OS")
+ @property
+ def permission(self):
+ """The permissions needed to modify this object type."""
+ return "environments.manage_environments"
- res = self.get_list()
- act_meta = res.json["meta"]
- exp_meta = {
- "limit": 20,
- "next": None,
- "offset": 0,
- "previous": None,
- "total_count": 1,
+ @property
+ def new_object_data(self):
+ """Generates a dictionary containing the field names and auto-generated
+ values needed to create a unique object.
+
+ The output of this method can be sent in the payload parameter of a
+ POST message.
+ """
+ modifiers = (self.datetime, self.resource_name)
+
+ return {
+ u"name": u"category %s %s" % modifiers,
+ u"elements": [],
+ }
+
+
+ def backend_object(self, id):
+ """Returns the object from the backend, so you can query it's values in
+ the database for validation.
+ """
+ return self.model.Category.everything.get(id=id)
+
+
+ def backend_data(self, backend_obj):
+ """Query's the database for the object's current values. Output is a
+ dictionary that should match the result of getting the object's detail
+ via the API, and can be used to verify API output.
+
+ Note: both keys and data should be in unicode
+ """
+ return {
+ u"id": unicode(str(backend_obj.id)),
+ u"name": unicode(backend_obj.name),
+ u"resource_uri": unicode(
+ self.get_detail_url(self.resource_name, str(backend_obj.id))),
+ u"elements": [{
+ u"name": unicode(elem.name),
+ u"id": unicode(elem.id),
+ u"resource_uri": unicode(self.get_detail_url(
+ "element", str(elem.id)
+ )),
+ } for elem in backend_obj.elements.all()]
}
-
- self.assertEquals(act_meta, exp_meta)
-
- act_objects = res.json["objects"]
- exp_objects = []
-
- exp_objects.append({
- u'id': unicode(category.id),
- u'name': u'OS',
- u'resource_uri': unicode(self.get_detail_url(
- "category",
- category.id,
- )),
- })
-
- self.maxDiff = None
- self.assertEqual(exp_objects, act_objects)
View
91 tests/model/environments/api/test_element_resource.py
@@ -3,60 +3,73 @@
"""
-from tests import case
+from tests.case.api.crud import ApiCrudCases
-class ElementResourceTest(case.api.ApiTestCase):
+class ElementResourceTest(ApiCrudCases):
@property
def factory(self):
"""The model factory for this object."""
- return self.F.ElementFactory
+ return self.F.ElementFactory()
@property
def resource_name(self):
return "element"
- def test_element_list(self):
- """Get a list of existing elements"""
- category = self.F.CategoryFactory.create(name="OS")
- element = self.factory.create(name="OS X", category=category)
+ @property
+ def permission(self):
+ """The permissions needed to modify this object type."""
+ return "environments.manage_environments"
- res = self.get_list()
- act_meta = res.json["meta"]
- exp_meta = {
- "limit": 20,
- "next": None,
- "offset": 0,
- "previous": None,
- "total_count": 1,
+ @property
+ def new_object_data(self):
+ """Generates a dictionary containing the field names and auto-generated
+ values needed to create a unique object.
+
+ The output of this method can be sent in the payload parameter of a
+ POST message.
+ """
+ modifiers = (self.datetime, self.resource_name)
+ self.category_fixture = self.F.CategoryFactory()
+
+ return {
+ u"name": u"element %s %s" % modifiers,
+ u"category": unicode(
+ self.get_detail_url("category", str(self.category_fixture.id))),
}
- self.assertEquals(act_meta, exp_meta)
-
- act_objects = res.json["objects"]
- exp_objects = []
-
- exp_objects.append({
- u'category': {
- u'id': unicode(category.id),
- u'name': u'OS',
- u'resource_uri': unicode(self.get_detail_url(
- "category",
- category.id,
- )),
- },
- u'id': unicode(element.id),
- u'name': u'OS X',
- u'resource_uri': unicode(self.get_detail_url(
- "element",
- element.id,
- )),
- })
-
- self.maxDiff = None
- self.assertEqual(exp_objects, act_objects)
+
+ @property
+ def read_create_fields(self):
+ """category is read-only."""
+ return ["category"]
+
+
+ def backend_object(self, id):
+ """Returns the object from the backend, so you can query it's values in
+ the database for validation.
+ """
+ return self.model.Element.everything.get(id=id)
+
+
+ def backend_data(self, backend_obj):
+ """Query's the database for the object's current values. Output is a
+ dictionary that should match the result of getting the object's detail
+ via the API, and can be used to verify API output.
+
+ Note: both keys and data should be in unicode
+ """
+ return {
+ u"id": unicode(str(backend_obj.id)),
+ u"name": unicode(backend_obj.name),
+ u"category": unicode(
+ self.get_detail_url("category", str(backend_obj.category.id))
+ ),
+ u"resource_uri": unicode(
+ self.get_detail_url(self.resource_name, str(backend_obj.id))),
+ }
View
10 tests/model/environments/api/test_environment_resource.py
@@ -45,14 +45,8 @@ def test_environment_list(self):
exp_objects.append({
u'elements': [{
- u'category': {
- u'id': unicode(category.id),
- u'name': u'OS',
- u'resource_uri': unicode(self.get_detail_url(
- "category",
- category.id,
- )),
- },
+ u'category': unicode(
+ self.get_detail_url("category", category.id)),
u'id': unicode(element.id),
u'name': u'OS X',
u'resource_uri': unicode(self.get_detail_url(
View
66 tests/model/environments/api/test_profile_resource.py
@@ -0,0 +1,66 @@
+"""
+Tests for ProfileResource api.
+
+"""
+
+from tests.case.api.crud import ApiCrudCases
+
+import logging
+mozlogger = logging.getLogger('moztrap.test')
+
+
+class ProfileResourceTest(ApiCrudCases):
+
+ @property
+ def factory(self):
+ """The model factory for this object."""
+ return self.F.ProfileFactory()
+
+
+ @property
+ def resource_name(self):
+ """The resource name for this object."""
+ return "profile"
+
+
+ @property
+ def permission(self):
+ """The permissions needed to modify this object type."""
+ return "environments.manage_environments"
+
+
+ @property
+ def new_object_data(self):
+ """Generates a dictionary containing the field names and auto-generated
+ values needed to create a unique object.
+
+ The output of this method can be sent in the payload parameter of a
+ POST message.
+ """
+ modifiers = (self.datetime, self.resource_name)
+
+ return {
+ u"name": u"profile %s %s" % modifiers,
+ }
+
+
+ def backend_object(self, id):
+ """Returns the object from the backend, so you can query it's values in
+ the database for validation.
+ """
+ return self.model.Profile.everything.get(id=id)
+
+
+ def backend_data(self, backend_obj):
+ """Query's the database for the object's current values. Output is a
+ dictionary that should match the result of getting the object's detail
+ via the API, and can be used to verify API output.
+
+ Note: both keys and data should be in unicode
+ """
+ return {
+ u"id": unicode(str(backend_obj.id)),
+ u"name": unicode(backend_obj.name),
+ u"resource_uri": unicode(
+ self.get_detail_url(self.resource_name, str(backend_obj.id))),
+ }
View
12 tests/model/execution/api/test_run.py
@@ -96,14 +96,10 @@ def test_run_by_id_shows_env_detail(self):
exp_objects = {
u'elements': [
- {u'category':
- {u'resource_uri': unicode(self.get_detail_url(
- "category",
- envs[0].elements.get().category.id),
- ),
- u'id': unicode(envs[0].elements.get().category.id),
- u'name': u'OS'
- },
+ {u'category': unicode(
+ self.get_detail_url(
+ "category",
+ envs[0].elements.get().category.id)),
u'resource_uri': unicode(self.get_detail_url(
"element",
envs[0].elements.get().id),
View
217 tests/model/library/api/test_case_resource.py
@@ -81,13 +81,10 @@ def backend_data(self, backend_obj):
return actual
- def manipulate_edit_data(self, fixture, fields):
- """product is read-only"""
- fields[u'product'] = unicode(
- self.get_detail_url('product', str(fixture.product.id)))
-
- return fields
-
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["product"]
# overrides from crud.py
@@ -100,212 +97,6 @@ def _ro_message(self):
return "product of an existing case may not be changed."
- def test_update_change_product_error(self):
- """product is a read-only field"""
-
- mozlogger.info("test_update_change_product_error")
-
- # fixtures
- fixture1 = self.factory
- prod = self.F.ProductFactory()
- fields = self.backend_data(fixture1)
- fields[u'product'] = unicode(
- self.get_detail_url("product", prod.id))
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- status=400,
- )
-
- self.assertEqual(res.text, self._ro_message)
-
-
-
-
-class SuiteCaseResourceTest(ApiCrudCases):
-
- @property
- def factory(self):
- """The model factory for this object."""
- return self.F.SuiteCaseFactory()
-
-
- @property
- def resource_name(self):
- return "suitecase"
-
-
- @property
- def permission(self):
- return "library.manage_suite_cases"
-
-
- @property
- def new_object_data(self):
- self.product_fixture = self.F.ProductFactory.create()
- self.case_fixture = self.F.CaseFactory.create(
- product=self.product_fixture)
- self.suite_fixture = self.F.SuiteFactory.create(
- product=self.product_fixture)
-
- fields = {
- u"case": unicode(
- self.get_detail_url("case", str(self.case_fixture.id))),
- u"suite": unicode(
- self.get_detail_url("suite", str(self.suite_fixture.id))),
- u"order": 1,
- }
-
- return fields
-
-
- def backend_object(self, id):
- """Returns the object from the backend, so you can query it's values in
- the database for validation.
- """
- return self.model.SuiteCase.everything.get(id=id)
-
-
- def backend_data(self, backend_obj):
- """Query's the database for the object's current values. Output is a
- dictionary that should match the result of getting the object's detail
- via the API, and can be used to verify API output.
-
- Note: both keys and data should be in unicode
- """
- actual = {}
- actual[u"resource_uri"] = unicode(
- self.get_detail_url(self.resource_name, str(backend_obj.id)))
- actual[u"id"] = unicode(str(backend_obj.id))
- actual[u"case"] = unicode(
- self.get_detail_url("case", str(backend_obj.case.id)))
- actual[u"suite"] = unicode(
- self.get_detail_url("suite", str(backend_obj.suite.id)))
- actual[u"order"] = backend_obj.order
-
- return actual
-
-
- def manipulate_edit_data(self, fixture, fields):
- """make sure order, the only thing editable, is different,
- but suite and case do not change"""
- fields[u'order'] = int(fields[u"order"]) + 1
- fields[u'suite'] = unicode(
- self.get_detail_url("suite", str(fixture.suite.id)))
- fields[u'case'] = unicode(
- self.get_detail_url("case", str(fixture.case.id)))
-
- return fields
-
- # overrides from crud.py
-
- # additional test cases, if any
-
- # validation cases
-
- def test_create_mismatched_product_error(self):
- """error if suite.product does not match case.product"""
-
- mozlogger.info("test_create_mismatched_product_error")
-
- fields = self.new_object_data
- product = self.F.ProductFactory()
- self.case_fixture.product = product
- self.case_fixture.save()
-
- # do post
- res = self.post(
- self.get_list_url(self.resource_name),
- params=self.credentials,
- payload=fields,
- status=400,
- )
-
- error_message = str(
- "case's product must match suite's product."
- )
- self.assertEqual(res.text, error_message)
-
-
- def test_update_change_case_error(self):
- """case is a read-only field"""
-
- mozlogger.info("test_update_change_case_error")
-
- # fixtures
- fixture1 = self.factory
- case = self.F.CaseFactory()
- case.product = fixture1.case.product
- case.save()
- fields = self.backend_data(fixture1)
- fields[u'case'] = unicode(
- self.get_detail_url("case", case.id))
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- status=400,
- )
-
- error_message = str(
- "case of an existing suitecase " +
- "may not be changed.")
- self.assertEqual(res.text, error_message)
-
-
- def test_update_change_suite_error(self):
- """case is a read-only field"""
-
- mozlogger.info("test_update_change_suite_error")
-
- # fixtures
- fixture1 = self.factory
- suite = self.F.SuiteFactory()
- suite.product = fixture1.suite.product
- suite.save()
- fields = self.backend_data(fixture1)
- fields[u'suite'] = unicode(
- self.get_detail_url("suite", suite.id))
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- status=400,
- )
-
- error_message = str(
- "suite of an existing suitecase " +
- "may not be changed.")
- self.assertEqual(res.text, error_message)
-
-
- def test_update_without_suite_and_case(self):
- """suite and case cannot be changed,
- so they are not required on edit."""
-
- mozlogger.info('test_update_without_suite_and_case')
-
- # fixtures
- fixture1 = self.factory
- fields = self.backend_data(fixture1)
- fields.pop('case')
- fields.pop('suite')
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- )
-
-
class CaseSelectionResourceTest(case.api.ApiTestCase):
View
50 tests/model/library/api/test_casestep_resource.py
@@ -80,57 +80,13 @@ def backend_data(self, backend_obj):
return actual
- def manipulate_edit_data(self, fixture, fields):
+ @property
+ def read_create_fields(self):
"""caseversion is read-only"""
- fields[u'caseversion'] = unicode(
- self.get_detail_url('caseversion', str(fixture.caseversion.id)))
-
- return fields
+ return ["caseversion"]
# overrides from crud.py
# additional test cases, if any
# validation cases
-
- def test_change_caseversion_should_error(self):
- """caseversion is a create-only field."""
-
- mozlogger.info('test_change_caseversion_should_error')
-
- # fixtures
- fixture1 = self.factory
- cv = self.F.CaseVersionFactory()
- fields = self.backend_data(fixture1)
- fields[u'caseversion'] = unicode(
- self.get_detail_url("caseversion", cv.id))
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- status=400,
- )
-
- self.assertEqual(res.text,
- "caseversion of an existing casestep may not be changed.")
-
-
- def test_update_without_caseversion(self):
- """caseversion cannot be changed,
- so it is not required on edit."""
-
- mozlogger.info('test_update_without_caseversion')
-
- # fixtures
- fixture1 = self.factory
- fields = self.backend_data(fixture1)
- fields.pop('caseversion')
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- )
View
87 tests/model/library/api/test_caseversion_resource.py
@@ -102,15 +102,11 @@ def backend_data(self, backend_obj):
return actual
- def manipulate_edit_data(self, fixture, fields):
- """case and productversion are read-only"""
- fields[u'case'] = unicode(
- self.get_detail_url('case', str(fixture.case.id)))
- fields[u'productversion'] = unicode(
- self.get_detail_url(
- 'productversion', str(fixture.productversion.id)))
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["case", "productversion"]
- return fields
# overrides from crud.py
@@ -145,81 +141,6 @@ def test_create_mismatched_product(self):
self.assertEqual(res.text, self._product_mismatch_message)
- def test_change_productversion_should_error(self):
- """productversion is a create-only field."""
-
- mozlogger.info('test_change_productversion_should_error')
-
- # fixtures
- fixture1 = self.factory
- pv = self.F.ProductVersionFactory()
- pv.product = fixture1.case.product
- pv.save()
- fields = self.backend_data(fixture1)
- fields[u'steps'] = self.new_object_data[u'steps']
- fields[u'productversion'] = unicode(
- self.get_detail_url("productversion", pv.id))
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- status=400,
- )
-
- self.assertEqual(res.text,
- "productversion of an existing caseversion may not be changed.")
-
-
- def test_change_case_should_error(self):
- """case is a create-only field."""
-
- mozlogger.info('test_change_case_should_error')
-
- # fixtures
- fixture1 = self.factory
- case = self.F.CaseFactory()
- case.product = fixture1.case.product
- case.save()
- fields = self.backend_data(fixture1)
- fields[u'steps'] = self.new_object_data[u'steps']
- fields[u'case'] = unicode(
- self.get_detail_url("case", case.id))
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- status=400,
- )
-
- self.assertEqual(res.text,
- "case of an existing caseversion may not be changed.")
-
-
- def test_update_without_productversion_and_case(self):
- """productversion and case cannot be changed,
- so they are not required on edit."""
-
- mozlogger.info('test_update_without_productversion_and_case')
-
- # fixtures
- fixture1 = self.factory
- fields = self.backend_data(fixture1)
- fields[u'steps'] = self.new_object_data[u'steps']
- fields.pop('case')
- fields.pop('productversion')
-
- # do put
- res = self.put(
- self.get_detail_url(self.resource_name, fixture1.id),
- params=self.credentials,
- data=fields,
- )
-
-
class CaseVersionSelectionResourceTest(case.api.ApiTestCase):
View
5 tests/model/library/api/test_suite_resource.py
@@ -94,6 +94,11 @@ def backend_data(self, backend_obj):
return actual
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["product"]
+
# additional test cases, if any
View
140 tests/model/library/api/test_suitecase_resource.py
@@ -0,0 +1,140 @@
+"""
+Tests for SuiteCaseResource api.
+
+"""
+from tests import case
+from tests.case.api.crud import ApiCrudCases
+
+import logging
+mozlogger = logging.getLogger('moztrap.test')
+
+
+class SuiteCaseResourceTest(ApiCrudCases):
+ """Please see the test cases implemented in tests.case.api.ApiCrudCases.
+
+ The following abstract methods must be implemented:
+ - factory(self) (property)
+ - resource_name(self) (property)
+ - permission(self) (property)
+ - new_object_data(self) (property)
+ - backend_object(self, id) (method)
+ - backend_data(self, backend_object) (method)
+ - backend_meta_data(self, backend_object) (method)
+
+ """
+
+ # implementations for abstract methods and properties
+
+ @property
+ def factory(self):
+ """The factory to use to create fixtures of the object under test.
+ """
+ return self.F.SuiteCaseFactory()
+
+
+ @property
+ def resource_name(self):
+ """String defining the resource name.
+ """
+ return "suitecase"
+
+
+ @property
+ def permission(self):
+ """String defining the permission required for
+ Create, Update, and Delete.
+ """
+ return "library.manage_suite_cases"
+
+
+ def order_generator(self):
+ """give an incrementing number for order."""
+ self.__dict__.setdefault("order", 0)
+ self.order = self.order + 1
+ return self.order
+
+
+ @property
+ def new_object_data(self):
+ """Generates a dictionary containing the field names and auto-generated
+ values needed to create a unique object.
+
+ The output of this method can be sent in the payload parameter of a
+ POST message.
+ """
+ self.product_fixture = self.F.ProductFactory.create()
+ self.case_fixture = self.F.CaseFactory.create(
+ product=self.product_fixture)
+ self.suite_fixture = self.F.SuiteFactory.create(
+ product=self.product_fixture)
+
+ fields = {
+ u"case": unicode(
+ self.get_detail_url("case", str(self.case_fixture.id))),
+ u"suite": unicode(
+ self.get_detail_url("suite", str(self.suite_fixture.id))
+ ),
+ u"order": self.order_generator(),
+ }
+ return fields
+
+
+ def backend_object(self, id):
+ """Returns the object from the backend, so you can query it's values in
+ the database for validation.
+ """
+ return self.model.SuiteCase.everything.get(id=id)
+
+
+ def backend_data(self, backend_obj):
+ """Query's the database for the object's current values. Output is a
+ dictionary that should match the result of getting the object's detail
+ via the API, and can be used to verify API output.
+
+ Note: both keys and data should be in unicode
+ """
+ return {
+ u"id": unicode(str(backend_obj.id)),
+ u"resource_uri": unicode(
+ self.get_detail_url(self.resource_name, str(backend_obj.id))),
+ u"case": unicode(
+ self.get_detail_url("case", str(backend_obj.case.id))),
+ u"suite": unicode(
+ self.get_detail_url("suite", str(backend_obj.suite.id))),
+ u"order": backend_obj.order,
+ }
+
+
+ @property
+ def read_create_fields(self):
+ """List of fields that are required for create but read-only for update."""
+ return ["suite", "case"]
+
+ # overrides from crud.py
+
+ # additional test cases, if any
+
+ # validation cases
+
+ def test_create_mismatched_product_error(self):
+ """error if suite.product does not match case.product"""
+
+ mozlogger.info("test_create_mismatched_product_error")
+
+ fields = self.new_object_data
+ product = self.F.ProductFactory()
+ self.case_fixture.product = product
+ self.case_fixture.save()
+
+ # do post
+ res = self.post(
+ self.get_list_url(self.resource_name),
+ params=self.credentials,
+ payload=fields,
+ status=400,
+ )
+
+ error_message = str(
+ "case's product must match suite's product."
+ )
+ self.assertEqual(res.text, error_message)

0 comments on commit 2893b95

Please sign in to comment.