4 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/f238040cc9a2/ Changeset: f238040cc9a2 User: jmchilton Date: 2014-01-10 15:45:36 Summary: Rework key injection logic in test framework. Pull logic for how master and user API keys are determinined for testing out of functional_tests.py for reuse elsewhere. This refactoring will help the creation of an API test framework. Affected #: 2 files diff -r 66df8b51245cd141fb7180e401e725ef3b1a0e37 -r f238040cc9a261db98edfa316384b33851f22486 scripts/functional_tests.py --- a/scripts/functional_tests.py +++ b/scripts/functional_tests.py @@ -51,6 +51,8 @@ from galaxy.util.json import to_json_string from functional import database_contexts +from base.api_util import get_master_api_key +from base.api_util import get_user_api_key import nose.core import nose.config @@ -64,8 +66,6 @@ default_galaxy_test_port_max = 9999 default_galaxy_locales = 'en' default_galaxy_test_file_dir = "test-data" -default_galaxy_master_key = "TEST123" -default_galaxy_user_key = None migrated_tool_panel_config = 'migrated_tools_conf.xml' installed_tool_panel_configs = [ 'shed_tool_conf.xml' ] @@ -342,7 +342,7 @@ galaxy_data_manager_data_path = tempfile.mkdtemp( prefix='data_manager_tool-data', dir=data_manager_test_tmp_path ) # ---- Build Application -------------------------------------------------- - master_api_key = os.environ.get( "GALAXY_TEST_MASTER_API_KEY", default_galaxy_master_key ) + master_api_key = get_master_api_key() app = None if start_server: kwargs = dict( admin_users='test@bx.psu.edu', @@ -477,7 +477,7 @@ import functional.test_workflow functional.test_workflow.WorkflowTestCase.workflow_test_file = workflow_test functional.test_workflow.WorkflowTestCase.master_api_key = master_api_key - functional.test_workflow.WorkflowTestCase.user_api_key = os.environ.get( "GALAXY_TEST_USER_API_KEY", default_galaxy_user_key ) + functional.test_workflow.WorkflowTestCase.user_api_key = get_user_api_key() data_manager_test = __check_arg( '-data_managers', param=False ) if data_manager_test: import functional.test_data_managers @@ -486,7 +486,7 @@ tmp_dir=data_manager_test_tmp_path, testing_shed_tools=testing_shed_tools, master_api_key=master_api_key, - user_api_key=os.environ.get( "GALAXY_TEST_USER_API_KEY", default_galaxy_user_key ), + user_api_key=get_user_api_key(), ) else: # We must make sure that functional.test_toolbox is always imported after @@ -499,7 +499,7 @@ functional.test_toolbox.build_tests( testing_shed_tools=testing_shed_tools, master_api_key=master_api_key, - user_api_key=os.environ.get( "GALAXY_TEST_USER_API_KEY", default_galaxy_user_key ), + user_api_key=get_user_api_key(), ) test_config = nose.config.Config( env=os.environ, ignoreFiles=ignore_files, plugins=nose.plugins.manager.DefaultPluginManager() ) test_config.configure( sys.argv ) diff -r 66df8b51245cd141fb7180e401e725ef3b1a0e37 -r f238040cc9a261db98edfa316384b33851f22486 test/base/api_util.py --- /dev/null +++ b/test/base/api_util.py @@ -0,0 +1,20 @@ +import os + +DEFAULT_GALAXY_MASTER_API_KEY = "TEST123" +DEFAULT_GALAXY_USER_API_KEY = None + + +def get_master_api_key(): + """ Test master API key to use for functional test. This key should be + configured as a master API key and should be able to create additional + users and keys. + """ + return os.environ.get( "GALAXY_TEST_MASTER_API_KEY", DEFAULT_GALAXY_MASTER_API_KEY ) + + +def get_user_api_key(): + """ Test user API key to use for functional tests. If set, this should drive + API based testing - if not set master API key should be used to create a new + user and API key for tests. + """ + return os.environ.get( "GALAXY_TEST_USER_API_KEY", DEFAULT_GALAXY_USER_API_KEY ) https://bitbucket.org/galaxy/galaxy-central/commits/efa607692e17/ Changeset: efa607692e17 User: jmchilton Date: 2014-01-10 15:45:36 Summary: Flush out API interactor... ... this will enable functional testing of API. Affected #: 1 file diff -r f238040cc9a261db98edfa316384b33851f22486 -r efa607692e171a5aa820d0a4b5ac9c8471431886 test/base/interactor.py --- a/test/base/interactor.py +++ b/test/base/interactor.py @@ -31,10 +31,11 @@ class GalaxyInteractorApi( object ): - def __init__( self, twill_test_case ): + def __init__( self, twill_test_case, test_user=None ): self.twill_test_case = twill_test_case self.api_url = "%s/api" % twill_test_case.url.rstrip("/") - self.api_key = self.__get_user_key( twill_test_case.user_api_key, twill_test_case.master_api_key ) + self.master_api_key = twill_test_case.master_api_key + self.api_key = self.__get_user_key( twill_test_case.user_api_key, twill_test_case.master_api_key, test_user=test_user ) self.uploads = {} def verify_output( self, history_id, output_data, outfile, attributes, shed_tool_id, maxseconds ): @@ -251,19 +252,26 @@ ) return self._post( "tools", files=files, data=data ) - def __get_user_key( self, user_key, admin_key ): - if user_key: - return user_key + def ensure_user_with_email( self, email ): + admin_key = self.master_api_key all_users = self._get( 'users', key=admin_key ).json() try: - test_user = [ user for user in all_users if user["email"] == 'test@bx.psu.edu' ][0] + test_user = [ user for user in all_users if user["email"] == email ][0] except IndexError: data = dict( - email='test@bx.psu.edu', + email=email, password='testuser', username='admin-user', ) test_user = self._post( 'users', data, key=admin_key ).json() + return test_user + + def __get_user_key( self, user_key, admin_key, test_user=None ): + if not test_user: + test_user = "test@bx.psu.edu" + if user_key: + return user_key + test_user = self.ensure_user_with_email(test_user) return self._post( "users/%s/api_key" % test_user['id'], key=admin_key ).json() def __dataset_fetcher( self, history_id ): @@ -275,16 +283,16 @@ return fetcher - def _post( self, path, data={}, files=None, key=None): + def _post( self, path, data={}, files=None, key=None, admin=False): if not key: - key = self.api_key + key = self.api_key if not admin else self.master_api_key data = data.copy() data['key'] = key return post_request( "%s/%s" % (self.api_url, path), data=data, files=files ) - def _get( self, path, data={}, key=None ): + def _get( self, path, data={}, key=None, admin=False ): if not key: - key = self.api_key + key = self.api_key if not admin else self.master_api_key data = data.copy() data['key'] = key if path.startswith("/api"): @@ -413,14 +421,17 @@ try: from requests import get as get_request from requests import post as post_request + from requests import put as put_request + from requests import delete as delete_request except ImportError: import urllib2 import httplib class RequestsLikeResponse( object ): - def __init__( self, content ): + def __init__( self, content, status_code ): self.content = content + self.status_code = status_code def json( self ): return loads( self.content ) @@ -431,23 +442,44 @@ argsep = '?' url = url + argsep + '&'.join( [ '%s=%s' % (k, v) for k, v in params.iteritems() ] ) #req = urllib2.Request( url, headers = { 'Content-Type': 'application/json' } ) - return RequestsLikeResponse(urllib2.urlopen( url ).read() ) + try: + response = urllib2.urlopen( url ) + return RequestsLikeResponse( response.read(), status_code=response.getcode() ) + except urllib2.HTTPError as e: + return RequestsLikeResponse( e.read(), status_code=e.code ) - def post_request( url, data, files ): + def post_request( url, data, files={} ): + return __multipart_request( url, data, files, verb="POST" ) + + def put_request( url, data, files={} ): + return __multipart_request( url, data, files, verb="PUT" ) + + def delete_request( url ): + opener = urllib2.build_opener(urllib2.HTTPHandler) + request = urllib2.Request(url) + request.get_method = lambda: 'DELETE' + try: + response = opener.open(request) + return RequestsLikeResponse( response.read(), status_code=response.getcode() ) + except urllib2.HTTPError as e: + return RequestsLikeResponse( e.read(), status_code=e.code ) + + + def __multipart_request( url, data, files={}, verb="POST" ): parsed_url = urllib2.urlparse.urlparse( url ) - return __post_multipart( host=parsed_url.netloc, selector=parsed_url.path, fields=data.iteritems(), files=(files or {}).iteritems() ) + return __multipart( host=parsed_url.netloc, selector=parsed_url.path, fields=data.iteritems(), files=(files or {}).iteritems(), verb=verb ) # http://stackoverflow.com/a/681182 - def __post_multipart(host, selector, fields, files): + def __multipart(host, selector, fields, files, verb="POST"): + h = httplib.HTTP(host) + h.putrequest(verb, selector) content_type, body = __encode_multipart_formdata(fields, files) - h = httplib.HTTP(host) - h.putrequest('POST', selector) h.putheader('content-type', content_type) h.putheader('content-length', str(len(body))) h.endheaders() h.send(body) errcode, errmsg, headers = h.getreply() - return RequestsLikeResponse(h.file.read()) + return RequestsLikeResponse(h.file.read(), status_code=errcode) def __encode_multipart_formdata(fields, files): LIMIT = '----------lImIt_of_THE_fIle_eW_$' https://bitbucket.org/galaxy/galaxy-central/commits/1cd7fd029a01/ Changeset: 1cd7fd029a01 User: jmchilton Date: 2014-01-10 15:45:36 Summary: Eliminate interactor dependency on deprecated global variable. Lessens the need to import things in a specific order - as long as database context is run before interactor methods everything should be fine now. TODO: Eliminate rest of dependencies on this variable. Affected #: 1 file diff -r efa607692e171a5aa820d0a4b5ac9c8471431886 -r 1cd7fd029a0195fe8ee4c3e5d423591fb4403a8f test/base/interactor.py --- a/test/base/interactor.py +++ b/test/base/interactor.py @@ -4,7 +4,7 @@ from galaxy.util.odict import odict import galaxy.model from galaxy.model.orm import and_, desc -from base.test_db_util import sa_session +from functional import database_contexts from json import dumps, loads from logging import getLogger @@ -391,9 +391,9 @@ # Start with a new history self.twill_test_case.logout() self.twill_test_case.login( email='test@bx.psu.edu' ) - admin_user = sa_session.query( galaxy.model.User ).filter( galaxy.model.User.table.c.email == 'test@bx.psu.edu' ).one() + admin_user = database_contexts.galaxy_context.query( galaxy.model.User ).filter( galaxy.model.User.table.c.email == 'test@bx.psu.edu' ).one() self.twill_test_case.new_history() - latest_history = sa_session.query( galaxy.model.History ) \ + latest_history = database_contexts.galaxy_context.query( galaxy.model.History ) \ .filter( and_( galaxy.model.History.table.c.deleted == False, galaxy.model.History.table.c.user_id == admin_user.id ) ) \ .order_by( desc( galaxy.model.History.table.c.create_time ) ) \ @@ -417,7 +417,7 @@ # Lets just try to use requests if it is available, but if not provide fallback -# on custom implementations of limited requests get/post functionality. +# on custom implementations of limited requests get, put, etc... functionality. try: from requests import get as get_request from requests import post as post_request @@ -464,7 +464,6 @@ except urllib2.HTTPError as e: return RequestsLikeResponse( e.read(), status_code=e.code ) - def __multipart_request( url, data, files={}, verb="POST" ): parsed_url = urllib2.urlparse.urlparse( url ) return __multipart( host=parsed_url.netloc, selector=parsed_url.path, fields=data.iteritems(), files=(files or {}).iteritems(), verb=verb ) https://bitbucket.org/galaxy/galaxy-central/commits/95314eb284fa/ Changeset: 95314eb284fa User: jmchilton Date: 2014-01-10 15:45:36 Summary: Implement framework for testing API... ... if you can call 1 new class a framework. Includes a few test cases to exercise/drive it. These examples include a histories API test (a typical API test) and a general test of the API framework itself (mostly just the run_as functionality). This includes changes to interactor.py to make it more useful outside the context of tool/workflow testing as well as a tweak to test Galaxy that gets started to allow testing of the run_as feature. Affected #: 5 files diff -r 1cd7fd029a0195fe8ee4c3e5d423591fb4403a8f -r 95314eb284faf3024207f70ef0dafe63c285e379 scripts/functional_tests.py --- a/scripts/functional_tests.py +++ b/scripts/functional_tests.py @@ -346,6 +346,7 @@ app = None if start_server: kwargs = dict( admin_users='test@bx.psu.edu', + api_allow_run_as='test@bx.psu.edu', allow_library_path_paste=True, allow_user_creation=True, allow_user_deletion=True, diff -r 1cd7fd029a0195fe8ee4c3e5d423591fb4403a8f -r 95314eb284faf3024207f70ef0dafe63c285e379 test/base/api.py --- /dev/null +++ b/test/base/api.py @@ -0,0 +1,85 @@ +# TODO: We don't need all of TwillTestCase, strip down to a common super class +# shared by API and Twill test cases. +from .twilltestcase import TwillTestCase + +from base.interactor import GalaxyInteractorApi as BaseInteractor + +from .api_util import get_master_api_key +from .api_util import get_user_api_key + +from urllib import urlencode + + +TEST_USER = "user@bx.psu.edu" + + +# TODO: Allow these to point at existing Galaxy instances. +class ApiTestCase( TwillTestCase ): + + def setUp( self ): + super( ApiTestCase, self ).setUp( ) + self.user_api_key = get_user_api_key() + self.master_api_key = get_master_api_key() + self.galaxy_interactor = ApiTestInteractor( self ) + + def _api_url( self, path, params=None, use_key=None ): + if not params: + params = {} + url = "%s/api/%s" % ( self.url, path ) + if use_key: + params[ "key" ] = self.galaxy_interactor.api_key + query = urlencode( params ) + if query: + url = "%s?%s" % ( url, query ) + return url + + def _setup_user( self, email ): + self.galaxy_interactor.ensure_user_with_email(email) + users = self._get( "users", admin=True ).json() + user = [ user for user in users if user["email"] == email ][0] + return user + + def _get( self, *args, **kwds ): + return self.galaxy_interactor.get( *args, **kwds ) + + def _post( self, *args, **kwds ): + return self.galaxy_interactor.post( *args, **kwds ) + + def _assert_status_code_is( self, response, expected_status_code ): + response_status_code = response.status_code + if expected_status_code != response_status_code: + try: + body = response.json() + except Exception: + body = "INVALID JSON RESPONSE" + assertion_message_template = "Request status code (%d) was not expected value %d. Body was %s" + assertion_message = assertion_message_template % ( response_status_code, expected_status_code, body ) + raise AssertionError( assertion_message ) + + def _assert_has_keys( self, response, *keys ): + for key in keys: + assert key in response, "Response [%s] does not contain key [%s]" % ( response, key ) + + def _random_key( self ): # Used for invalid request testing... + return "1234567890123456" + + _assert_has_key = _assert_has_keys + + +class ApiTestInteractor( BaseInteractor ): + """ Specialized variant of the API interactor (originally developed for + tool functional tests) for testing the API generally. + """ + + def __init__( self, test_case ): + super( ApiTestInteractor, self ).__init__( test_case, test_user=TEST_USER ) + + # This variant the lower level get and post methods are meant to be used + # directly to test API - instead of relying on higher-level constructs for + # specific pieces of the API (the way it is done with the variant for tool) + # testing. + def get( self, *args, **kwds ): + return self._get( *args, **kwds ) + + def post( self, *args, **kwds ): + return self._post( *args, **kwds ) diff -r 1cd7fd029a0195fe8ee4c3e5d423591fb4403a8f -r 95314eb284faf3024207f70ef0dafe63c285e379 test/functional/api/test_framework.py --- /dev/null +++ b/test/functional/api/test_framework.py @@ -0,0 +1,27 @@ +# This file doesn't test any API in particular but is meant to functionally +# test the API framework itself. +from base import api + + +class ApiFrameworkTestCase( api.ApiTestCase ): + + # Next several tests test the API's run_as functionality. + def test_user_cannont_run_as( self ): + post_data = dict( name="TestHistory1", run_as="another_user" ) + # Normal user cannot run_as... + create_response = self._post( "histories", data=post_data ) + self._assert_status_code_is( create_response, 403 ) + + def test_run_as_invalid_user( self ): + post_data = dict( name="TestHistory1", run_as="another_user" ) + # admin user can run_as, but this user doesn't exist, expect 400. + create_response = self._post( "histories", data=post_data, admin=True ) + self._assert_status_code_is( create_response, 400 ) + + def test_run_as_valid_user( self ): + run_as_user = self._setup_user( "for_run_as@bx.psu.edu" ) + post_data = dict( name="TestHistory1", run_as=run_as_user[ "id" ] ) + # Use run_as with admin user and for another user just created, this + # should work. + create_response = self._post( "histories", data=post_data, admin=True ) + self._assert_status_code_is( create_response, 200 ) diff -r 1cd7fd029a0195fe8ee4c3e5d423591fb4403a8f -r 95314eb284faf3024207f70ef0dafe63c285e379 test/functional/api/test_histories.py --- /dev/null +++ b/test/functional/api/test_histories.py @@ -0,0 +1,26 @@ +from base import api +# requests.post or something like it if unavailable +from base.interactor import post_request + + +class HistoriesApiTestCase( api.ApiTestCase ): + + def test_create_history( self ): + # Create a history. + post_data = dict( name="TestHistory1" ) + create_response = self._post( "histories", data=post_data ).json() + self._assert_has_keys( create_response, "name", "id" ) + self.assertEquals( create_response[ "name" ], "TestHistory1" ) + created_id = create_response[ "id" ] + + # Make sure new history appears in index of user's histories. + index_response = self._get( "histories" ).json() + indexed_history = [ h for h in index_response if h[ "id" ] == created_id ][0] + self.assertEquals(indexed_history[ "name" ], "TestHistory1") + + def test_create_anonymous_fails( self ): + post_data = dict( name="CannotCreate" ) + # Using lower-level _api_url will cause key to not be injected. + histories_url = self._api_url( "histories" ) + create_response = post_request( url=histories_url, data=post_data ) + self._assert_status_code_is( create_response, 403 ) Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.