Source

restontrac / trac-dev / restontrac / tests / test_resource_routes.py

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you under the Apache License, Version 2.0 (the
#  "License"); you may not use this file except in compliance
#  with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing,
#  software distributed under the License is distributed on an
#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#  KIND, either express or implied.  See the License for the
#  specific language governing permissions and limitations
#  under the License.

"""Tests for url mappings to IRestResource instances """

import unittest, tempfile
from trac.test import EnvironmentStub
#TODO absolute imports
from restontrac.tracrest.api import RestSystem
from restontrac.tracrest.web_ui import RestModule
from restontrac.tracrest.resources import RestResourceBase

class NestedResource(RestResourceBase):
    """ Sample implementation for validating the routing of
        nested resources like ticket->attachments
        Or in the future Product -> Tickets
    """
    def __init__(self):
        self.setup("attachments","attachment",
                    resource_name='project_attachments',
                    parent_collection="/projects",
                    parent_resource="project")

class AutoconfigResource(RestResourceBase):
    """ Sample implementation for validating the routing of
        autoconfigurable resources as the case of Tickets
        Wich will have a collection and a member resource
        providing a standard API and implementation for this
        combination of Collection/member resources
    """
    def __init__(self):
        self.setup("tickets","ticket")

class CustomMappingResource(RestResourceBase):
    """ Sample implementation for validating the routing of
        some custom special resources, that won't provide the
        standard set of functionalities, but could rather
        publish some single Http method instead of the complete
        list
        Verifies the mapping of a custom /prototype url since the
        internal mapping here does not take place using
        routes.Mapper.resource() but util.map_resource_path()
    """
    def __init__(self):
        self.setup("milestones","milestone")

    def supported_paths(self):
        collection_url = "".join(["/",self.collection_name])
        return {collection_url:('GET','POST'),
                collection_url + "/{id}":('GET'),
                collection_url + "/prototype":('GET')
               }
    def get_custom_actions_map(self):
        return {(self.collection_name + "/prototype",'GET'): "prototype"}


class ResourceRouteTests(unittest.TestCase):
    """Unit tests covering the Product model"""
    def setUp(self):
        self.env = EnvironmentStub(enable=['trac.*', 'tracrest.*','api.*','resources','util',
                                           'web_ui.*','NestedResource','AutoconfigResource',
                                           'CustomMappingResource','RestModule','RestSystem'])
        self.env.enable_component(NestedResource)
        self.env.enable_component(AutoconfigResource)
        self.env.enable_component(CustomMappingResource)
        self.env.enable_component(RestModule)
        self.env.enable_component(RestSystem)
        self.rest_module = RestModule(self.env)
        self.mapper = RestModule(self.env)._mapper
        self.env.path = tempfile.mkdtemp('tracrest-tempenv')
        #For the moment there is database dependency within these test cases
        #self.env.reset_db()

    def _verify_match(self, path, match_expected, method, action=None, id=None):
        """Utility operation for the execution of the tests
           and verification of results
        """
        match = self.mapper.match(path, {"REQUEST_METHOD": method})
        route = self.mapper.routematch(path, {"REQUEST_METHOD": method})
        call_type = ""
        if route:
            call_type = 'COLLECTION CALL' if ':(id)' in route[1].routepath else 'MEMBER CALL'
        match_data = match or 'NOT SUPPORTED'
        #TODO log this line
        print "%s %s-> %s %s" % (call_type, method, path, str(match_data))
        if match_expected:
            self.assertTrue(match)
            if action:
                self.assertEquals(match.get('action'),action)
            if id:
                self.assertEqual(match.get('id'),id)
        else:
            self.assertFalse(match)

        return match

    def test_nested_resources(self):
        """Test case for the functioning of the mapping of nested resources
           Configurations tested in this test case were provided
           by NestedResource test component within the present module
        """
        #TODO document expected results within comments
        route_vars = self._verify_match("/projects/26/attachments/51",True,'GET')
        self.assertEquals(route_vars.get('controller'),'project_attachments')
        self.assertEquals(route_vars.get('project_id'),'26')
        self.assertEquals(route_vars.get('id'),'51')

        self._verify_match("/projects/",False,'GET')

        self._verify_match("/projects/26/attachments",True,'POST')
        self._verify_match("/projects/26/attachments",True,'GET')
        self._verify_match("/projects/26/attachments/45",True,'PUT')
        self._verify_match("/projects/26/attachments",False,'PUT')

    def test_resource_autoconfig(self):
        """Test case to verify the expected functioning of resources url
           auto-configuration.
           Configurations tested in this test case were provided
           by AutoconfigResource test component within the present module
        """
        #verifying the reading of the collection, by default operation will be called
        # 'list' for auto-configured resources
        self._verify_match("/tickets", True, "GET",action='list')
        # Since no "new" resource url is supported, in a call to "resource/new"
        # 'new' must be interpreted as the member id matching resource/{id}
        match = self._verify_match("/tickets/new", True, "GET",action='read', id='new')
        self.assertTrue(match.get("id") == 'new')
        #No post to the member allowed, only GET, PUT, DELETE
        self._verify_match("/tickets/new", False, "POST")
        #verifying the route mapping for create operation in the collection
        self._verify_match("/tickets", True, "POST",action='create')
        #No post allowed to the member resource
        self._verify_match("/tickets/25", False, "POST")
        #route mapping for to read the member
        match = self._verify_match("/tickets/25", True, "GET", action='read',id='25')
        #route mapping for update operations on the member
        self._verify_match("/tickets/25", True, "PUT", action='update',id='25')
        #verifying delete method on the member
        self._verify_match("/tickets/25", True, "DELETE", action='delete',id='25')
        #No update allowed for the collection
        self._verify_match("/tickets", False, "PUT")
        #No delete allowed for the collection
        self._verify_match("/tickets", False, "DELETE")

        # for the moment will allow to specify the format as extensions
        # although request header content-type will be supported as well
        match = self._verify_match("/tickets.xml", True, "GET")
        self.assertTrue(match.get('format') == 'xml')

        #checking the format does not interfere with id extraction for read paths
        match = self._verify_match("/tickets/25.xml", True, "GET",id='25')

        #checking the format does not interfere with id extraction for update paths
        match = self._verify_match("/tickets/25.xml", True, "PUT", id='25')

        # checking that auto-configured resources will have the
        # /prototype resource within the collection resource to obtain
        # a copyable instance of the member resource initialized with
        # default values to be used in create operations.
        # /prototype is a symbolic resource that one should GET, to have
        # default values already filled in, to change only desired values
        # and send it back to the server within a POST to the collection
        match = self._verify_match("/tickets/prototype", True, "GET", action='prototype')
        #/prototype shouldn't be interpreted as an id but as a particular resource
        self.assertFalse(match.get('id',None))

    def test_resource_supported_paths(self):
        """Special test case to verify the functioning of
           custom paths configuration that do not take place automatically
           by RestResourceBase.map_resources().
           When the resource does not accomplish the
           standard interface of a collection/member resource
           then it must provide a list of supported paths allong with
           the supported http methods for each one
           Additional the resource might implement get_custom_actions_map()
           to override the default mapping from http methods to actions so
           it's possible to indicate that a call to GET in some url should
           be forwarded to an operation 'do_something()' instead of 'read()'
           Configurations tested in this test case were provided
           by CustomMappingResource test component within the present module
        """
        self._verify_match("/milestones", True, "GET")
        #a call to resource/new must be interpreted as /resource{id}
        #with 'new' as the id value
        self._verify_match("/milestones/new", True, "GET",id='new')
        #No post to the member configured
        self._verify_match("/milestones/new", False, "POST")
        #POST was configured for the collection, must be allowed
        self._verify_match("/milestones", True, "POST")
        #No post to the member configured
        self._verify_match("/milestones/25", False, "POST")
        #GET was configured, must be allowed
        self._verify_match("/milestones/25", True, "GET")
        #No put allowed for the configuration provided
        self._verify_match("/milestones/25", False, "PUT")
        #No delete allowed for the configuration provided
        self._verify_match("/milestones/25", False, "DELETE")
        #No put allowed for the configuration provided
        self._verify_match("/milestones", False, "PUT")
        #No delete allowed for the configuration provided
        self._verify_match("/milestones", False, "DELETE")
        #No configuration of format provided
        self._verify_match("/milestones.xml", False, "GET")
        #As configured this resource allows no format so any extension will
        # be interpreted as part if the id
        self._verify_match("/milestones/25.xml", True, "GET", id='25.xml')
        #PUT wasn't included in supported_paths()
        self._verify_match("/milestones/25.xml", False, "PUT")

        # verifying 'prototype' is not interpreted as an id,
        # since it was provided as a supported path instead
        match = self._verify_match("/milestones/prototype", True, "GET")
        self.assertFalse(match.get('id',None))

        #The custom action 'prototype' was configured through
        # get_custom_actions_map() of CustomMappingResource
        self.assertTrue(match.get('action','prototype'))

        # only GET configured in this test case,
        # '/prototype' is not a member but a special resource
        # to copy it before creating a new member
        self._verify_match("/milestones/prototype", False, "PUT")

    def tearDown(self):
        pass