| #!/usr/bin/env python |
| |
| # Copyright (C) 2009 Google Inc. |
| # |
| # Licensed 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. |
| |
| |
| import sys |
| import unittest |
| import getpass |
| import inspect |
| import atom.mock_http_core |
| import gdata.gauth |
| |
| |
| """Loads configuration for tests which connect to Google servers. |
| |
| Settings used in tests are stored in a ConfigCollection instance in this |
| module called options. If your test needs to get a test related setting, |
| use |
| |
| import gdata.test_config |
| option_value = gdata.test_config.options.get_value('x') |
| |
| The above will check the command line for an '--x' argument, and if not |
| found will either use the default value for 'x' or prompt the user to enter |
| one. |
| |
| Your test can override the value specified by the user by performing: |
| |
| gdata.test_config.options.set_value('x', 'y') |
| |
| If your test uses a new option which you would like to allow the user to |
| specify on the command line or via a prompt, you can use the register_option |
| method as follows: |
| |
| gdata.test_config.options.register( |
| 'option_name', 'Prompt shown to the user', secret=False #As for password. |
| 'This is the description of the option, shown when help is requested.', |
| 'default value, provide only if you do not want the user to be prompted') |
| """ |
| |
| |
| class Option(object): |
| |
| def __init__(self, name, prompt, secret=False, description=None, default=None): |
| self.name = name |
| self.prompt = prompt |
| self.secret = secret |
| self.description = description |
| self.default = default |
| |
| def get(self): |
| value = self.default |
| # Check for a command line parameter. |
| for i in xrange(len(sys.argv)): |
| if sys.argv[i].startswith('--%s=' % self.name): |
| value = sys.argv[i].split('=')[1] |
| elif sys.argv[i] == '--%s' % self.name: |
| value = sys.argv[i + 1] |
| # If the param was not on the command line, ask the user to input the |
| # value. |
| # In order for this to prompt the user, the default value for the option |
| # must be None. |
| if value is None: |
| prompt = '%s: ' % self.prompt |
| if self.secret: |
| value = getpass.getpass(prompt) |
| else: |
| print 'You can specify this on the command line using --%s' % self.name |
| value = raw_input(prompt) |
| return value |
| |
| |
| class ConfigCollection(object): |
| |
| def __init__(self, options=None): |
| self.options = options or {} |
| self.values = {} |
| |
| def register_option(self, option): |
| self.options[option.name] = option |
| |
| def register(self, *args, **kwargs): |
| self.register_option(Option(*args, **kwargs)) |
| |
| def get_value(self, option_name): |
| if option_name in self.values: |
| return self.values[option_name] |
| value = self.options[option_name].get() |
| if value is not None: |
| self.values[option_name] = value |
| return value |
| |
| def set_value(self, option_name, value): |
| self.values[option_name] = value |
| |
| def render_usage(self): |
| message_parts = [] |
| for opt_name, option in self.options.iteritems(): |
| message_parts.append('--%s: %s' % (opt_name, option.description)) |
| return '\n'.join(message_parts) |
| |
| |
| options = ConfigCollection() |
| |
| |
| # Register the default options. |
| options.register( |
| 'username', |
| 'Please enter the email address of your test account', |
| description=('The email address you want to sign in with. ' |
| 'Make sure this is a test account as these tests may edit' |
| ' or delete data.')) |
| options.register( |
| 'password', |
| 'Please enter the password for your test account', |
| secret=True, description='The test account password.') |
| options.register( |
| 'clearcache', |
| 'Delete cached data? (enter true or false)', |
| description=('If set to true, any temporary files which cache test' |
| ' requests and responses will be deleted.'), |
| default='true') |
| options.register( |
| 'savecache', |
| 'Save requests and responses in a temporary file? (enter true or false)', |
| description=('If set to true, requests to the server and responses will' |
| ' be saved in temporary files.'), |
| default='false') |
| options.register( |
| 'runlive', |
| 'Run the live tests which contact the server? (enter true or false)', |
| description=('If set to true, the tests will make real HTTP requests to' |
| ' the servers. This slows down test execution and may' |
| ' modify the users data, be sure to use a test account.'), |
| default='true') |
| options.register( |
| 'ssl', |
| 'Run the live tests over SSL (enter true or false)', |
| description='If set to true, all tests will be performed over HTTPS (SSL)', |
| default='false') |
| options.register( |
| 'appsusername', |
| 'Please enter the email address of your test Apps domain account', |
| description=('The email address you want to sign in with. ' |
| 'Make sure this is a test account on your Apps domain as ' |
| 'these tests may edit or delete data.')) |
| options.register( |
| 'appspassword', |
| 'Please enter the password for your test Apps domain account', |
| secret=True, description='The test Apps account password.') |
| |
| # Other options which may be used if needed. |
| BLOG_ID_OPTION = Option( |
| 'blogid', |
| 'Please enter the ID of your test blog', |
| description=('The blog ID for the blog which should have test posts added' |
| ' to it. Example 7682659670455539811')) |
| TEST_IMAGE_LOCATION_OPTION = Option( |
| 'imgpath', |
| 'Please enter the full path to a test image to upload', |
| description=('This test image will be uploaded to a service which' |
| ' accepts a media file, it must be a jpeg.')) |
| SPREADSHEET_ID_OPTION = Option( |
| 'spreadsheetid', |
| 'Please enter the ID of a spreadsheet to use in these tests', |
| description=('The spreadsheet ID for the spreadsheet which should be' |
| ' modified by theses tests.')) |
| APPS_DOMAIN_OPTION = Option( |
| 'appsdomain', |
| 'Please enter your Google Apps domain', |
| description=('The domain the Google Apps is hosted on or leave blank' |
| ' if n/a')) |
| SITES_NAME_OPTION = Option( |
| 'sitename', |
| 'Please enter name of your Google Site', |
| description='The webspace name of the Site found in its URL.') |
| PROJECT_NAME_OPTION = Option( |
| 'project_name', |
| 'Please enter the name of your project hosting project', |
| description=('The name of the project which should have test issues added' |
| ' to it. Example gdata-python-client')) |
| ISSUE_ASSIGNEE_OPTION = Option( |
| 'issue_assignee', |
| 'Enter the email address of the target owner of the updated issue.', |
| description=('The email address of the user a created issue\'s owner will ' |
| ' become. Example testuser2@gmail.com')) |
| GA_TABLE_ID = Option( |
| 'table_id', |
| 'Enter the Table ID of the Google Analytics profile to test', |
| description=('The Table ID of the Google Analytics profile to test.' |
| ' Example ga:1174')) |
| TARGET_USERNAME_OPTION = Option( |
| 'targetusername', |
| 'Please enter the username (without domain) of the user which will be' |
| ' affected by the tests', |
| description=('The username of the user to be tested')) |
| YT_DEVELOPER_KEY_OPTION = Option( |
| 'developerkey', |
| 'Please enter your YouTube developer key', |
| description=('The YouTube developer key for your account')) |
| YT_CLIENT_ID_OPTION = Option( |
| 'clientid', |
| 'Please enter your YouTube client ID', |
| description=('The YouTube client ID for your account')) |
| YT_VIDEO_ID_OPTION= Option( |
| 'videoid', |
| 'Please enter the ID of a YouTube video you uploaded', |
| description=('The video ID of a YouTube video uploaded to your account')) |
| |
| |
| # Functions to inject a cachable HTTP client into a service client. |
| def configure_client(client, case_name, service_name, use_apps_auth=False): |
| """Sets up a mock client which will reuse a saved session. |
| |
| Should be called during setUp of each unit test. |
| |
| Handles authentication to allow the GDClient to make requests which |
| require an auth header. |
| |
| Args: |
| client: a gdata.GDClient whose http_client member should be replaced |
| with a atom.mock_http_core.MockHttpClient so that repeated |
| executions can used cached responses instead of contacting |
| the server. |
| case_name: str The name of the test case class. Examples: 'BloggerTest', |
| 'ContactsTest'. Used to save a session |
| for the ClientLogin auth token request, so the case_name |
| should be reused if and only if the same username, password, |
| and service are being used. |
| service_name: str The service name as used for ClientLogin to identify |
| the Google Data API being accessed. Example: 'blogger', |
| 'wise', etc. |
| use_apps_auth: bool (optional) If set to True, use appsusername and |
| appspassword command-line args instead of username and |
| password respectively. |
| """ |
| # Use a mock HTTP client which will record and replay the HTTP traffic |
| # from these tests. |
| client.http_client = atom.mock_http_core.MockHttpClient() |
| client.http_client.cache_case_name = case_name |
| # Getting the auth token only needs to be done once in the course of test |
| # runs. |
| auth_token_key = '%s_auth_token' % service_name |
| if (auth_token_key not in options.values |
| and options.get_value('runlive') == 'true'): |
| client.http_client.cache_test_name = 'client_login' |
| cache_name = client.http_client.get_cache_file_name() |
| if options.get_value('clearcache') == 'true': |
| client.http_client.delete_session(cache_name) |
| client.http_client.use_cached_session(cache_name) |
| if not use_apps_auth: |
| username = options.get_value('username') |
| password = options.get_value('password') |
| else: |
| username = options.get_value('appsusername') |
| password = options.get_value('appspassword') |
| auth_token = client.request_client_login_token(username, password, |
| case_name, service=service_name) |
| options.values[auth_token_key] = gdata.gauth.token_to_blob(auth_token) |
| client.http_client.close_session() |
| # Allow a config auth_token of False to prevent the client's auth header |
| # from being modified. |
| if auth_token_key in options.values: |
| client.auth_token = gdata.gauth.token_from_blob( |
| options.values[auth_token_key]) |
| |
| |
| def configure_cache(client, test_name): |
| """Loads or begins a cached session to record HTTP traffic. |
| |
| Should be called at the beginning of each test method. |
| |
| Args: |
| client: a gdata.GDClient whose http_client member has been replaced |
| with a atom.mock_http_core.MockHttpClient so that repeated |
| executions can used cached responses instead of contacting |
| the server. |
| test_name: str The name of this test method. Examples: |
| 'TestClass.test_x_works', 'TestClass.test_crud_operations'. |
| This is used to name the recording of the HTTP requests and |
| responses, so it should be unique to each test method in the |
| test case. |
| """ |
| # Auth token is obtained in configure_client which is called as part of |
| # setUp. |
| client.http_client.cache_test_name = test_name |
| cache_name = client.http_client.get_cache_file_name() |
| if options.get_value('clearcache') == 'true': |
| client.http_client.delete_session(cache_name) |
| client.http_client.use_cached_session(cache_name) |
| |
| |
| def close_client(client): |
| """Saves the recoded responses to a temp file if the config file allows. |
| |
| This should be called in the unit test's tearDown method. |
| |
| Checks to see if the 'savecache' option is set to 'true', to make sure we |
| only save sessions to repeat if the user desires. |
| """ |
| if client and options.get_value('savecache') == 'true': |
| # If this was a live request, save the recording. |
| client.http_client.close_session() |
| |
| |
| def configure_service(service, case_name, service_name): |
| """Sets up a mock GDataService v1 client to reuse recorded sessions. |
| |
| Should be called during setUp of each unit test. This is a duplicate of |
| configure_client, modified to handle old v1 service classes. |
| """ |
| service.http_client.v2_http_client = atom.mock_http_core.MockHttpClient() |
| service.http_client.v2_http_client.cache_case_name = case_name |
| # Getting the auth token only needs to be done once in the course of test |
| # runs. |
| auth_token_key = 'service_%s_auth_token' % service_name |
| if (auth_token_key not in options.values |
| and options.get_value('runlive') == 'true'): |
| service.http_client.v2_http_client.cache_test_name = 'client_login' |
| cache_name = service.http_client.v2_http_client.get_cache_file_name() |
| if options.get_value('clearcache') == 'true': |
| service.http_client.v2_http_client.delete_session(cache_name) |
| service.http_client.v2_http_client.use_cached_session(cache_name) |
| service.ClientLogin(options.get_value('username'), |
| options.get_value('password'), |
| service=service_name, source=case_name) |
| options.values[auth_token_key] = service.GetClientLoginToken() |
| service.http_client.v2_http_client.close_session() |
| if auth_token_key in options.values: |
| service.SetClientLoginToken(options.values[auth_token_key]) |
| |
| |
| def configure_service_cache(service, test_name): |
| """Loads or starts a session recording for a v1 Service object. |
| |
| Duplicates the behavior of configure_cache, but the target for this |
| function is a v1 Service object instead of a v2 Client. |
| """ |
| service.http_client.v2_http_client.cache_test_name = test_name |
| cache_name = service.http_client.v2_http_client.get_cache_file_name() |
| if options.get_value('clearcache') == 'true': |
| service.http_client.v2_http_client.delete_session(cache_name) |
| service.http_client.v2_http_client.use_cached_session(cache_name) |
| |
| |
| def close_service(service): |
| if service and options.get_value('savecache') == 'true': |
| # If this was a live request, save the recording. |
| service.http_client.v2_http_client.close_session() |
| |
| |
| def build_suite(classes): |
| """Creates a TestSuite for all unit test classes in the list. |
| |
| Assumes that each of the classes in the list has unit test methods which |
| begin with 'test'. Calls unittest.makeSuite. |
| |
| Returns: |
| A new unittest.TestSuite containing a test suite for all classes. |
| """ |
| suites = [unittest.makeSuite(a_class, 'test') for a_class in classes] |
| return unittest.TestSuite(suites) |
| |
| |
| def check_data_classes(test, classes): |
| import inspect |
| for data_class in classes: |
| test.assert_(data_class.__doc__ is not None, |
| 'The class %s should have a docstring' % data_class) |
| if hasattr(data_class, '_qname'): |
| qname_versions = None |
| if isinstance(data_class._qname, tuple): |
| qname_versions = data_class._qname |
| else: |
| qname_versions = (data_class._qname,) |
| for versioned_qname in qname_versions: |
| test.assert_(isinstance(versioned_qname, str), |
| 'The class %s has a non-string _qname' % data_class) |
| test.assert_(not versioned_qname.endswith('}'), |
| 'The _qname for class %s is only a namespace' % ( |
| data_class)) |
| |
| for attribute_name, value in data_class.__dict__.iteritems(): |
| # Ignore all elements that start with _ (private members) |
| if not attribute_name.startswith('_'): |
| try: |
| if not (isinstance(value, str) or inspect.isfunction(value) |
| or (isinstance(value, list) |
| and issubclass(value[0], atom.core.XmlElement)) |
| or type(value) == property # Allow properties. |
| or inspect.ismethod(value) # Allow methods. |
| or issubclass(value, atom.core.XmlElement)): |
| test.fail( |
| 'XmlElement member should have an attribute, XML class,' |
| ' or list of XML classes as attributes.') |
| |
| except TypeError: |
| test.fail('Element %s in %s was of type %s' % ( |
| attribute_name, data_class._qname, type(value))) |
| |
| |
| def check_clients_with_auth(test, classes): |
| for client_class in classes: |
| test.assert_(hasattr(client_class, 'api_version')) |
| test.assert_(isinstance(client_class.auth_service, (str, unicode, int))) |
| test.assert_(hasattr(client_class, 'auth_service')) |
| test.assert_(isinstance(client_class.auth_service, (str, unicode))) |
| test.assert_(hasattr(client_class, 'auth_scopes')) |
| test.assert_(isinstance(client_class.auth_scopes, (list, tuple))) |