diff --git a/README.rst b/README.rst index 9bb989b..96ac24d 100644 --- a/README.rst +++ b/README.rst @@ -35,22 +35,19 @@ Flask `mit_lti_flask_sample Dependencies: ============= -* Python 2.7+ or Python 3.4+ -* oauth2 1.9.0+ -* httplib2 0.9+ -* six 1.10.0+ +* Python 3.7+ +* oauthlib 3.x +* requests-oauthlib 1.x Development dependencies: ========================= -* Flask 0.10.1 -* httpretty 0.8.3 -* oauthlib 0.6.3 -* pyflakes 1.2.3 -* pytest 2.9.2 -* pytest-cache 1.0 -* pytest-cov 2.3.0 -* pytest-flakes 1.0.1 -* pytest-pep8 1.0.6 +* Chalice 1.x +* Flask 2.x +* httpretty 1.x +* pytest 7.x +* pytest-cov 4.x +* pytest-flakes 4.x +* semantic-version 2.x * sphinx 1.2.3 Documentation_ is available on readthedocs. diff --git a/pylti/__init__.py b/pylti/__init__.py index 902a4ae..1d66783 100644 --- a/pylti/__init__.py +++ b/pylti/__init__.py @@ -3,4 +3,4 @@ PyLTI is module that implements IMS LTI in python The API uses decorators to wrap function with LTI functionality. """ -__version__ = '0.6.0' +__version__ = '0.8.0' diff --git a/pylti/common.py b/pylti/common.py index f609d1f..f790f25 100644 --- a/pylti/common.py +++ b/pylti/common.py @@ -7,16 +7,20 @@ import logging import json -import oauth2 -from xml.etree import ElementTree as etree +import string +import time +from urllib.parse import urlencode -from oauth2 import STRING_TYPES -from six.moves.urllib.parse import urlparse, urlencode +from oauthlib.oauth1 import RequestValidator, SignatureOnlyEndpoint +from oauthlib.oauth1.rfc5849 import CONTENT_TYPE_FORM_URLENCODED +from requests_oauthlib import OAuth1Session +from xml.etree import ElementTree as etree log = logging.getLogger('pylti.common') # pylint: disable=invalid-name LTI_PROPERTY_LIST = [ 'oauth_consumer_key', + 'lti_message_type', 'launch_presentation_return_url', 'user_id', 'oauth_nonce', @@ -35,7 +39,12 @@ 'lti_message', 'lti_version', 'roles', - 'lis_outcome_service_url' + 'lis_outcome_service_url', + 'accept_media_types', + 'accept_multiple', + 'accept_presentation_document_targets', + 'accept_unsigned', + 'content_item_return_url', ] @@ -59,57 +68,6 @@ def default_error(exception=None): return "There was an LTI communication error", 500 -class LTIOAuthServer(oauth2.Server): - """ - Largely taken from reference implementation - for app engine at https://code.google.com/p/ims-dev/ - """ - - def __init__(self, consumers, signature_methods=None): - """ - Create OAuth server - """ - super(LTIOAuthServer, self).__init__(signature_methods) - self.consumers = consumers - - def lookup_consumer(self, key): - """ - Search through keys - """ - if not self.consumers: - log.critical(("No consumers defined in settings." - "Have you created a configuration file?")) - return None - - consumer = self.consumers.get(key) - if not consumer: - log.info("Did not find consumer, using key: %s ", key) - return None - - secret = consumer.get('secret', None) - if not secret: - log.critical(('Consumer %s, is missing secret' - 'in settings file, and needs correction.'), key) - return None - return oauth2.Consumer(key, secret) - - def lookup_cert(self, key): - """ - Search through keys - """ - if not self.consumers: - log.critical(("No consumers defined in settings." - "Have you created a configuration file?")) - return None - - consumer = self.consumers.get(key) - if not consumer: - log.info("Did not find consumer, using key: %s ", key) - return None - cert = consumer.get('cert', None) - return cert - - class LTIException(Exception): """ Custom LTI exception for proper handling @@ -142,65 +100,144 @@ class LTIPostMessageException(LTIException): pass +# https://oauthlib.readthedocs.io/en/latest/oauth1/validator.html +class LTIRequestValidator(RequestValidator): + def __init__(self, consumers): + self._consumers = consumers + + @property + def enforce_ssl(self): + # for backward-compatibility, we won't require SSL + return False + + @property + def safe_characters(self): + # allowed characters in an LTI key + return set(string.printable) + + @property + def client_key_length(self): + # some very loose guidelines on how long a key should be + return 3, 1000 + + @property + def nonce_length(self): + # some very loose guidelines on how long a nonce should be + return 8, 1000 + + @property + def dummy_client(self): + """Dummy client used when an invalid client key is supplied. + + :returns: The dummy client key string. + """ + return "__dummy_invalid_client_key" + + def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, + request, request_token=None, access_token=None): + """Validates that the timestamp is valid. + + # NOTE: Nonce reuse is **not** checked, though it should be by the OAuth spec. + This implementation is attempting to match the python-oauth2 library, which + does not record nonces or check them for reuse. + + :param client_key: The client/consumer key. + :param timestamp: The ``oauth_timestamp`` parameter. + :param nonce: The ``oauth_nonce`` parameter. + :param request_token: Request token string, if any. + :param access_token: Access token string, if any. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :returns: True or False + + Per `Section 3.3`_ of the spec. + + "A nonce is a random string, uniquely generated by the client to allow + the server to verify that a request has never been made before and + helps prevent replay attacks when requests are made over a non-secure + channel. The nonce value MUST be unique across all requests with the + same timestamp, client credentials, and token combinations." + + .. _`Section 3.3`: https://tools.ietf.org/html/rfc5849#section-3.3 + + One of the first validation checks that will be made is for the validity + of the nonce and timestamp, which are associated with a client key and + possibly a token. If invalid then immediately fail the request + by returning False. If the nonce/timestamp pair has been used before and + you may just have detected a replay attack. Therefore it is an essential + part of OAuth security that you not allow nonce/timestamp reuse. + Note that this validation check is done before checking the validity of + the client and token.:: + + nonces_and_timestamps_database = [ + (u'foo', 1234567890, u'rannoMstrInghere', u'bar') + ] + + def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, + request_token=None, access_token=None): + + return ((client_key, timestamp, nonce, request_token or access_token) + not in self.nonces_and_timestamps_database) + """ + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_lifetime: + return False + + return True + + def validate_client_key(self, client_key, request): + """Validates that supplied client key is a registered and valid client. + + :param client_key: The client/consumer key. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :returns: True or False + """ + return client_key in self._consumers + + def get_client_secret(self, client_key, request): + """Retrieves the client secret associated with the client key. + + :param client_key: The client/consumer key. + :param request: OAuthlib request. + :type request: oauthlib.common.Request + :returns: The client secret as a string. + """ + consumer = self._consumers.get(client_key, None) + if consumer: + return consumer.get('secret', None) + else: + return None + + def _post_patched_request(consumers, lti_key, body, url, method, content_type): """ - Authorization header needs to be capitalized for some LTI clients - this function ensures that header is capitalized - :param body: body of the call :param client: OAuth Client :param url: outcome url :return: response """ - # pylint: disable=too-many-locals, too-many-arguments - oauth_server = LTIOAuthServer(consumers) - oauth_server.add_signature_method(SignatureMethod_HMAC_SHA1_Unicode()) - lti_consumer = oauth_server.lookup_consumer(lti_key) - lti_cert = oauth_server.lookup_cert(lti_key) - secret = lti_consumer.secret - - consumer = oauth2.Consumer(key=lti_key, secret=secret) - client = oauth2.Client(consumer) - - if lti_cert: - client.add_certificate(key=lti_cert, cert=lti_cert, domain='') - log.debug("cert %s", lti_cert) - - import httplib2 - - http = httplib2.Http - # pylint: disable=protected-access - normalize = http._normalize_headers - - def my_normalize(self, headers): - """ This function patches Authorization header """ - ret = normalize(self, headers) - if 'authorization' in ret: - ret['Authorization'] = ret.pop('authorization') - log.debug("headers") - log.debug(headers) - return ret - - http._normalize_headers = my_normalize - monkey_patch_function = normalize - response, content = client.request( - url, - method, - body=body.encode('utf-8'), - headers={'Content-Type': content_type}) + try: + consumer = consumers[lti_key] + except ValueError: + raise LTIException(f"Consumer {lti_key} not found in configured consumers.") - http = httplib2.Http - # pylint: disable=protected-access - http._normalize_headers = monkey_patch_function + secret = consumer.get('secret', None) + lti_cert = consumer.get('cert', None) + + session = OAuth1Session(lti_key, client_secret=secret) + response = session.request(method, url, data=body, headers={'Content-Type': content_type}, cert=lti_cert) log.debug("key %s", lti_key) log.debug("secret %s", secret) log.debug("url %s", url) - log.debug("response %s", response) - log.debug("content %s", format(content)) + log.debug("status %s", response.status_code) + log.debug("content %s", format(response.content)) - return response, content + return response, response.content def post_message(consumers, lti_key, url, body): @@ -250,7 +287,7 @@ def post_message2(consumers, lti_key, url, body, content_type, ) - is_success = response.status == 200 + is_success = response.status_code == 200 log.debug("is success %s", is_success) return is_success @@ -273,12 +310,6 @@ def verify_request_common(consumers, url, method, headers, params): log.debug("headers %s", headers) log.debug("params %s", params) - oauth_server = LTIOAuthServer(consumers) - oauth_server.add_signature_method( - SignatureMethod_PLAINTEXT_Unicode()) - oauth_server.add_signature_method( - SignatureMethod_HMAC_SHA1_Unicode()) - # Check header for SSL before selecting the url if ( headers.get( @@ -288,27 +319,29 @@ def verify_request_common(consumers, url, method, headers, params): ): url = url.replace('http:', 'https:', 1) - oauth_request = Request_Fix_Duplicate.from_request( - method, + headers = dict(headers) + body = None + if params: + params = dict(params) + if method == "POST": + headers['Content-Type'] = headers.get('Content-Type', CONTENT_TYPE_FORM_URLENCODED) + body = urlencode(params, True) + else: + url = f"{url}?{urlencode(params, True)}" + body = None + + validator = LTIRequestValidator(consumers) + endpoint = SignatureOnlyEndpoint(validator) + valid, request = endpoint.validate_request( url, - headers=dict(headers), - parameters=params - ) - if not oauth_request: - log.info('Received non oauth request on oauth protected page') - raise LTIException('This page requires a valid oauth session ' - 'or request') - try: - # pylint: disable=protected-access - oauth_consumer_key = oauth_request.get_parameter('oauth_consumer_key') - consumer = oauth_server.lookup_consumer(oauth_consumer_key) - if not consumer: - raise oauth2.Error('Invalid consumer.') - oauth_server.verify_request(oauth_request, consumer, None) - except oauth2.Error: - # Rethrow our own for nice error handling (don't print - # error message as it will contain the key - raise LTIException("OAuth error: Please check your key and secret") + method, + body=body, + headers=headers) + if not valid: + if request: + raise LTIException(f"OAuth error: Please check your key and secret. key={request.client_key}") + else: + raise LTIException("OAuth error: Error while validating request.") return True @@ -358,109 +391,6 @@ def generate_request_xml(message_identifier_id, operation, return ret -class SignatureMethod_HMAC_SHA1_Unicode(oauth2.SignatureMethod_HMAC_SHA1): - """ - Temporary workaround for - https://github.com/joestump/python-oauth2/issues/207 - - Original code is Copyright (c) 2007 Leah Culver, MIT license. - """ - - def check(self, request, consumer, token, signature): - """ - Returns whether the given signature is the correct signature for - the given consumer and token signing the given request. - """ - built = self.sign(request, consumer, token) - if isinstance(signature, STRING_TYPES): - signature = signature.encode("utf8") - return built == signature - - -class SignatureMethod_PLAINTEXT_Unicode(oauth2.SignatureMethod_PLAINTEXT): - """ - Temporary workaround for - https://github.com/joestump/python-oauth2/issues/207 - - Original code is Copyright (c) 2007 Leah Culver, MIT license. - """ - - def check(self, request, consumer, token, signature): - """ - Returns whether the given signature is the correct signature for - the given consumer and token signing the given request. - """ - built = self.sign(request, consumer, token) - if isinstance(signature, STRING_TYPES): - signature = signature.encode("utf8") - return built == signature - - -class Request_Fix_Duplicate(oauth2.Request): - """ - Temporary workaround for - https://github.com/joestump/python-oauth2/pull/197 - - Original code is Copyright (c) 2007 Leah Culver, MIT license. - """ - - def get_normalized_parameters(self): - """ - Return a string that contains the parameters that must be signed. - """ - items = [] - for key, value in self.items(): - if key == 'oauth_signature': - continue - # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, - # so we unpack sequence values into multiple items for sorting. - if isinstance(value, STRING_TYPES): - items.append( - (oauth2.to_utf8_if_string(key), oauth2.to_utf8(value)) - ) - else: - try: - value = list(value) - except TypeError as e: - assert 'is not iterable' in str(e) - items.append( - (oauth2.to_utf8_if_string(key), - oauth2.to_utf8_if_string(value)) - ) - else: - items.extend( - (oauth2.to_utf8_if_string(key), - oauth2.to_utf8_if_string(item)) - for item in value - ) - - # Include any query string parameters from the provided URL - query = urlparse(self.url)[4] - url_items = self._split_url_string(query).items() - url_items = [ - (oauth2.to_utf8(k), oauth2.to_utf8_optional_iterator(v)) - for k, v in url_items if k != 'oauth_signature' - ] - - # Merge together URL and POST parameters. - # Eliminates parameters duplicated between URL and POST. - items_dict = {} - for k, v in items: - items_dict.setdefault(k, []).append(v) - for k, v in url_items: - if not (k in items_dict and v in items_dict[k]): - items.append((k, v)) - - items.sort() - - encoded_str = urlencode(items, True) - # Encode signature parameters per Oauth Core 1.0 protocol - # spec draft 7, section 3.6 - # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) - # Spaces must be encoded with "%20" instead of "+" - return encoded_str.replace('+', '%20').replace('%7E', '~') - - class LTIBase(object): """ LTI Object represents abstraction of current LTI session. It provides diff --git a/pylti/flask.py b/pylti/flask.py index d6ed21c..6651697 100644 --- a/pylti/flask.py +++ b/pylti/flask.py @@ -62,7 +62,7 @@ def verify_request(self): log.debug(params) log.debug('verify_request?') try: - verify_request_common(self._consumers(), flask_request.url, + verify_request_common(self._consumers(), flask_request.base_url, flask_request.method, flask_request.headers, params) log.debug('verify_request success') diff --git a/pylti/tests/data/certs/snakeoil.pem b/pylti/tests/data/certs/snakeoil.pem index 7dab985..20daa45 100644 --- a/pylti/tests/data/certs/snakeoil.pem +++ b/pylti/tests/data/certs/snakeoil.pem @@ -1,30 +1,49 @@ -----BEGIN CERTIFICATE----- -MIICXTCCAcYCCQDRykEqQc08tzANBgkqhkiG9w0BAQUFADByMQswCQYDVQQGEwJV -UzEQMA4GA1UECAwHRXhhbXBsZTEQMA4GA1UEBwwHRXhhbXBsZTEQMA4GA1UECgwH -RXhhbXBsZTEQMA4GA1UECwwHRXhhbXBsZTEbMBkGA1UEAwwScGVyc29uQGV4YW1w -bGUuY29tMCAXDTE0MTIxNjIxMzIxMFoYDzIxMTQxMTIyMjEzMjEwWjByMQswCQYD -VQQGEwJVUzEQMA4GA1UECAwHRXhhbXBsZTEQMA4GA1UEBwwHRXhhbXBsZTEQMA4G -A1UECgwHRXhhbXBsZTEQMA4GA1UECwwHRXhhbXBsZTEbMBkGA1UEAwwScGVyc29u -QGV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+fMA1J4Wg -O6PPbx/m/fH1whw53oIN99wU86dPGguJ4BSgNmY3HOysuJvWpP5PCRIR65CoGmTO -5+xUPoAKNkHTlOZE3dsV/48/22IaxBHREPWnMH0+hKmKq2YSgpx0vOUpLgIpaTu5 -eYgN9755475LrruAp5iD+zihlRzbVnjj/QIDAQABMA0GCSqGSIb3DQEBBQUAA4GB -AEMBrxMTFV2Mbg/WIGadVGe7/n+sqyEBQ8ikpj4WSIWUqpgeRvA6ZRE8K+NJ+xTV -Nu1ppF7e8VWyCjvNtPhevMdwO91Lc6PTXi40k9zJYGz1DRdD8kmw8LQGcOd3fIQ7 -CfTLjGyGrdWuM5w9Y3YuKQqnachUq/F68uOEVqLXFoaP +MIIDazCCAlOgAwIBAgIUXW8SjhjJVh4xUvf3olFYPfrMh+MwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MDMwNTI3MTRaFw0zMzA0 +MzAwNTI3MTRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDdxgULL1zy9ZITYzLrPjrzDx4YIpF65yHJp1uKhe3a +QXfCTcmewCe29fn9+8vukH+YuAN8ecCzzUfVhrFdM6FGuI3RRSJuqSA32VjRir9l +Cl3JtqbwBEi4ebtav71CoRI8tfPlRh3uqFmiWGgGcTqYkcfRtAgJKSeOfRgg0yLY +wp4BxS3UKHK15ckYffNJH5RpPn++RIiFuNNSpeNbxb+o0vzb/OylAcnEeHe/bO8H +CxjpaVtm2SKLPzo9wCtXfx0TcFmbo3Q4sLz1ZPOW+acGXAOGassao2mGhl3Fg/Yd +YFhM1gjA2TIAdA5j/oIttbxGxvI0Y5OUghczEZRljudLAgMBAAGjUzBRMB0GA1Ud +DgQWBBRIPTTnkP7uZ9vyiS+Xc7FSz/oHKDAfBgNVHSMEGDAWgBRIPTTnkP7uZ9vy +iS+Xc7FSz/oHKDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB5 +CTNqVbzN+ZzGR7L15+10ncbQdYNh/oGQaRc4SRvl3E3zzdJnGcuCY/WcN0Hbd+bZ +ebpozR0lQYCAL6RHtHRAOG02teU63WaJcja0hfdMaJNfScvhEq8HDtTbBp9Oe2ay +Bqei+sIyRXNovuBQyljEhjY7YNEnrAdfS6T5JPZoj6T/VQWK8ziXsq83beFe6TWs +uH2Ww3Vsuw73UaYRgidF45c5SaIVeu3EtBcyZaC8sP84kdaV044HlUZL+nWnj33C +4U268VdPmrGJ3I7cOMntNzcZPTZdqkyA7kxARJRSYAgN7IbbFE5Pjs+8KJI4jBry +6J9JZ/6BuW2+Zs/pPKNU -----END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQC+fMA1J4WgO6PPbx/m/fH1whw53oIN99wU86dPGguJ4BSgNmY3 -HOysuJvWpP5PCRIR65CoGmTO5+xUPoAKNkHTlOZE3dsV/48/22IaxBHREPWnMH0+ -hKmKq2YSgpx0vOUpLgIpaTu5eYgN9755475LrruAp5iD+zihlRzbVnjj/QIDAQAB -AoGAEjHwWiNwTCHmP8YpkfLnzcXA1HZAjf0C9K1hadjfCUhyL+uCT/lfUhBAMnyI -HhyLsVKC+suqnWjh1hoyOMd9+gFcXPFVLMDhkP+88C8b7OBG2qRQKcjFkEJLrI1G -f6O8Lc6ukgBoJAh1Nuq0pf8tYKwS9nIndK2jK/0DCUBP6bECQQD5LC7b/uqeaiqk -YH9+bR6FABjwLOh78WKl/ZwEaston7QJAAcJVK5BerBqmy3Mr584+jc2/XylLMiH -s/kEQmg3AkEAw7TrvBbPCYkdqudv6/975z81yhh4WOY+RgYzD1Ol913EhtANJrng -0+QrVgefr4mw0TXQrGW/+KYzA07IVr7TawJBAOEZJhf+OWv1EyK+Pk8zOsACL4VB -vKDDl0/HRVvEMpAIvnbm/HRUeLuUn60fFQf1nAy4FotqAmGhjGLzlkFf0I8CQGSj -U5ncTMkFhokNDGPadDe9LIbpQHHOrHVL2NPn2u+ye04sDKc+bJvpuFM8BmS5NIDQ -4KbWh/pwVMk9qQ3agVMCQClReiBbizvqYnDXIizMhWHfwajul/ileMLyW0RO95Se -685fYoyrttDLfVtQQ9yFrghAhM3BdR07DwxQRYEgJQE= ------END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDdxgULL1zy9ZIT +YzLrPjrzDx4YIpF65yHJp1uKhe3aQXfCTcmewCe29fn9+8vukH+YuAN8ecCzzUfV +hrFdM6FGuI3RRSJuqSA32VjRir9lCl3JtqbwBEi4ebtav71CoRI8tfPlRh3uqFmi +WGgGcTqYkcfRtAgJKSeOfRgg0yLYwp4BxS3UKHK15ckYffNJH5RpPn++RIiFuNNS +peNbxb+o0vzb/OylAcnEeHe/bO8HCxjpaVtm2SKLPzo9wCtXfx0TcFmbo3Q4sLz1 +ZPOW+acGXAOGassao2mGhl3Fg/YdYFhM1gjA2TIAdA5j/oIttbxGxvI0Y5OUghcz +EZRljudLAgMBAAECggEAKseh/XBbe7qHPRornllYwb4uzfUNHyoqyF1yORxwr2Nl +mKOsuuTSRGbanHXP9usE0g7dDUvnMkftDXF4EUR+XcgIA1BUvgf94QgaLAGZvgHr +6ZdESJRq+rrwuya1eX7cp71pmNaXu4vaDokDOArrhLbKVtdlnW7E5KWY2+wekrVM +GVyol5pVepNcoLaB9vYJxblRsOvPHcDr9MRl6D4qF5967BpC2PKrtB37GP3wcmLB +QB+y/u09UHLxHU33iiDb/6S0unUQ6yB6uA+esHz7T/uwAeQZKvTT6qLTpdfma7JB +FK5dVa8CKrXvulecesqJPDr4HP9hUucPfleIubZX0QKBgQDyGUYJYr6alPmLdu1I +KIFlDHsdpg3Q8z0VCGkUe+XF7Vo+v0FVZFXUvhAICXDWjzOm/BxI0+eB6gct2SuS +awgXHCUYsUrp8ow/Q7TE30rlxgx9Q6Dhku4u1roW40F37yIzyRf26Au8kYz17YBp +vjDf4U56YjitaIRKTgTBYyBq5wKBgQDqgfnbqMxwjUiYwSHOdEAMYA/mPa+wiLjd +E4PU0KU+bKre2DkzzQOBOEUOKti1w2PdmTQ5MsB0pzUuh4KOy/nV4eVOyIhXuedM +ecqdHjwvRRwhV/fRVI5+X3i4RSETOoAkSw3F8vVlCVOqvjDYcAZhDv/FdcHLmSGD +ZkO4dPGX/QKBgFgovelDDPeLkken+gYRwfTDE74bLuLNAIw9MM6lw2lM4lUBHlBz +JhI/V+UlUvK+2OdQ3RfkGmSjjRO0BnreAOcxd4zDWu1QRqPvCs+6JDMB6KBg1R/v +ek6SINeez8NV0FWdP93IaCW1tugDIYTgHjoYeJR2Wf9DlRDd0jt91ls3AoGAV3aQ +tP94+IWJTQfDTxgGh1cQtwPM0h+8KyBLLAWBjA3FkQW+F/bf1sMg5k7OssQkLBMm +6ipmo1t1t1vtMssa7E2rU73xNB7vCJPoIL+VHOA+xKTlldpepv1+reOCmYRZJLAl +e+3I3p0i6myzFRZ7GpoYhRINbJ05ZaOvoE1lihUCgYBiFiosn+vbbm6gK+B0qBkB +ZmqCGm1BWBuYS98Es1ln3sYiIDpXhMEauxvDv+UTowB00aa2epGAfCUFncKOAiQZ +nETtwVsvhR2umIB6gw7jdzqYSUl94tmnsfsugBRV/DHaWapPl+pbtXD9KlXLw0WZ +NmBQYHQiyCnC1K/7HRQE5w== +-----END PRIVATE KEY----- diff --git a/pylti/tests/test_chalice.py b/pylti/tests/test_chalice.py index 362b1a1..ed1b726 100644 --- a/pylti/tests/test_chalice.py +++ b/pylti/tests/test_chalice.py @@ -9,7 +9,7 @@ import oauthlib.oauth1 import os -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode from pylti.common import LTIException # from pylti.chalice import LTI @@ -123,7 +123,7 @@ def generate_launch_request(consumers, url, SIGNATURE_HMAC, signature_type=oauthlib.oauth1. SIGNATURE_TYPE_QUERY) - signature = client.sign("{}{}".format(url, urlparams)) + signature = client.sign("{}?{}".format(url, urlparams)) signed_url = signature[0] new_url = signed_url[len('https://localhost'):] return new_url @@ -227,7 +227,7 @@ def test_access_to_oauth_resource_without_authorization_initial_get(self): self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) self.assertEqual(self.get_exception_as_string(), - 'This page requires a valid oauth session or request') + 'OAuth error: Error while validating request.') def test_access_without_authorization_post_form(self): """ @@ -243,7 +243,7 @@ def test_access_without_authorization_post_form(self): self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) self.assertEqual(self.get_exception_as_string(), - 'This page requires a valid oauth session or request') + 'OAuth error: Error while validating request.') # DELETE: No sessions in Chalice # def test_access_to_oauth_resource_in_session(self): @@ -271,7 +271,7 @@ def test_access_to_oauth_resource_get(self): Accessing oauth_resource. """ consumers = self.consumers - url = 'https://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) self.localGateway.handle_request(method='GET', path=new_url, @@ -287,7 +287,7 @@ def test_access_to_oauth_resource_post(self): Accessing oauth_resource. """ consumers = self.consumers - url = 'https://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) (path, body) = new_url.split("?") self.localGateway.handle_request(method='POST', @@ -305,7 +305,7 @@ def test_access_to_oauth_resource_name_passed(self): """ # pylint: disable=maybe-no-member consumers = self.consumers - url = 'https://localhost/name?' + url = 'https://localhost/name' add_params = {u'lis_person_sourcedid': u'person'} new_url = self.generate_launch_request( consumers, url, add_params=add_params @@ -326,7 +326,7 @@ def test_access_to_oauth_resource_email_passed(self): """ # pylint: disable=maybe-no-member consumers = self.consumers - url = 'https://localhost/name?' + url = 'https://localhost/name' add_params = {u'lis_person_contact_email_primary': u'email@email.com'} new_url = self.generate_launch_request( consumers, url, add_params=add_params @@ -347,7 +347,7 @@ def test_access_to_oauth_resource_name_and_email_passed(self): """ # pylint: disable=maybe-no-member consumers = self.consumers - url = 'https://localhost/name?' + url = 'https://localhost/name' add_params = {u'lis_person_sourcedid': u'person', u'lis_person_contact_email_primary': u'email@email.com'} new_url = self.generate_launch_request( @@ -368,7 +368,7 @@ def test_access_to_oauth_resource_staff_only_as_student(self): Deny access if user not in role. """ consumers = self.consumers - url = 'https://localhost/initial_staff?' + url = 'https://localhost/initial_staff' student_url = self.generate_launch_request( consumers, url, roles='Student' ) @@ -398,7 +398,7 @@ def test_access_to_oauth_resource_staff_only_as_administrator(self): Allow access if user in role. """ consumers = self.consumers - url = 'https://localhost/initial_staff?' + url = 'https://localhost/initial_staff' new_url = self.generate_launch_request( consumers, url, roles='Administrator' ) @@ -416,7 +416,7 @@ def test_access_to_oauth_resource_staff_only_as_unknown_role(self): Deny access if role not defined. """ consumers = self.consumers - url = 'https://localhost/initial_unknown?' + url = 'https://localhost/initial_unknown' admin_url = self.generate_launch_request( consumers, url, roles='Administrator' ) @@ -434,7 +434,7 @@ def test_access_to_oauth_resource_student_as_student(self): Verify that the various roles we consider as students are students. """ consumers = self.consumers - url = 'https://localhost/initial_student?' + url = 'https://localhost/initial_student' # Learner Role learner_url = self.generate_launch_request( @@ -464,7 +464,7 @@ def test_access_to_oauth_resource_student_as_student(self): def test_access_to_oauth_resource_student_as_staff(self): """Verify staff doesn't have access to student only.""" consumers = self.consumers - url = 'https://localhost/initial_student?' + url = 'https://localhost/initial_student' staff_url = self.generate_launch_request( consumers, url, roles='Staff' ) @@ -480,7 +480,7 @@ def test_access_to_oauth_resource_student_as_staff(self): def test_access_to_oauth_resource_student_as_unknown(self): """Verify unknown role doesn't have access to student only.""" consumers = self.consumers - url = 'https://localhost/initial_student?' + url = 'https://localhost/initial_student' unknown_url = self.generate_launch_request( consumers, url, roles='FooBar' ) @@ -499,7 +499,7 @@ def test_access_to_oauth_resource_student_as_unknown(self): # """ # Test access to LTI protected resources. # """ - # url = 'https://localhost/any?' + # url = 'https://localhost/any' # new_url = self.generate_launch_request(self.consumers, url) # self.localGateway.handle_request(method='GET', # path=new_url, @@ -514,7 +514,7 @@ def test_access_to_oauth_resource_initial_norole(self): """ Test access to LTI protected resources. """ - url = 'https://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(self.consumers, url, roles=None) self.localGateway.handle_request(method='GET', path=new_url, @@ -530,7 +530,7 @@ def test_access_to_oauth_resource_any_nonstandard_role(self): """ Test access to LTI protected resources. """ - url = 'https://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(self.consumers, url, roles=u'ThisIsNotAStandardRole') self.localGateway.handle_request(method='GET', @@ -547,7 +547,7 @@ def test_access_to_oauth_resource_invalid(self): Deny access to LTI protected resources on man in the middle attack. """ - url = 'https://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(self.consumers, url) bad_url = "{}&FAIL=TRUE".format(new_url) self.localGateway.handle_request(method='GET', @@ -559,7 +559,7 @@ def test_access_to_oauth_resource_invalid(self): body='') self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) - self.assertEqual(self.get_exception_as_string(), + self.assertEqual(self.get_exception_as_string()[:45], 'OAuth error: Please check your key and secret') # DELETE: Chalice does not support sessions @@ -571,13 +571,13 @@ def test_access_to_oauth_resource_invalid(self): # self.app.get('/session') # self.assertFalse(self.has_exception()) - # url = 'https://localhost/initial?' + # url = 'https://localhost/initial' # new_url = self.generate_launch_request(self.consumers, url) # self.app.get("{}&FAIL=TRUE".format(new_url)) # self.assertTrue(self.has_exception()) # self.assertIsInstance(self.get_exception(), LTIException) - # self.assertEqual(self.get_exception_as_string(), + # self.assertEqual(self.get_exception_as_string()[:45], # 'OAuth error: Please check your key and secret') # UPDATE: Original established a session and then called post_grade @@ -596,7 +596,7 @@ def test_access_to_oauth_resource_post_grade(self): httpretty.register_uri(httpretty.POST, uri, body=self.request_callback) consumers = self.consumers - url = 'https://localhost/post_grade/1.0?' + url = 'https://localhost/post_grade/1.0' new_url = self.generate_launch_request(consumers, url) ret = self.localGateway.handle_request(method='GET', path=new_url, @@ -608,7 +608,7 @@ def test_access_to_oauth_resource_post_grade(self): self.assertFalse(self.has_exception()) self.assertEqual(ret['body'], "grade=True") - url = 'https://localhost/post_grade/2.0?' + url = 'https://localhost/post_grade/2.0' new_url = self.generate_launch_request(consumers, url) ret = self.localGateway.handle_request(method='GET', path=new_url, @@ -643,7 +643,7 @@ def request_callback(request, cburi, headers): httpretty.register_uri(httpretty.POST, uri, body=request_callback) consumers = self.consumers - url = 'https://localhost/post_grade/1.0?' + url = 'https://localhost/post_grade/1.0' new_url = self.generate_launch_request(consumers, url) ret = self.localGateway.handle_request(method='GET', path=new_url, @@ -667,7 +667,7 @@ def request_callback(request, cburi, headers): # httpretty.register_uri(httpretty.POST, uri, # body=self.request_callback) - # url = 'https://localhost/initial?' + # url = 'https://localhost/initial' # new_url = self.generate_launch_request( # self.consumers, url, lit_outcome_service_url=uri # ) @@ -696,7 +696,7 @@ def test_access_to_oauth_resource_post_grade2(self): httpretty.register_uri(httpretty.PUT, uri, body=self.request_callback) consumers = self.consumers - url = 'https://localhost/post_grade2/1.0?' + url = 'https://localhost/post_grade2/1.0' new_url = self.generate_launch_request(consumers, url) ret = self.localGateway.handle_request(method='GET', path=new_url, @@ -708,7 +708,7 @@ def test_access_to_oauth_resource_post_grade2(self): self.assertFalse(self.has_exception()) self.assertEqual(ret['body'], "grade=True") - url = 'https://localhost/post_grade2/2.0?' + url = 'https://localhost/post_grade2/2.0' new_url = self.generate_launch_request(consumers, url) ret = self.localGateway.handle_request(method='GET', path=new_url, @@ -741,7 +741,7 @@ def request_callback(request, cburi, headers): httpretty.register_uri(httpretty.PUT, uri, body=request_callback) consumers = self.consumers - url = 'https://localhost/post_grade2/1.0?' + url = 'https://localhost/post_grade2/1.0' new_url = self.generate_launch_request(consumers, url) ret = self.localGateway.handle_request(method='GET', path=new_url, @@ -757,7 +757,7 @@ def test_default_decorator(self): """ Verify default decorator works. """ - url = 'https://localhost/default_lti?' + url = 'https://localhost/default_lti' new_url = self.generate_launch_request(self.consumers, url) self.localGateway.handle_request(method='GET', path=new_url, diff --git a/pylti/tests/test_common.py b/pylti/tests/test_common.py index 0547f3a..d6d1ea1 100644 --- a/pylti/tests/test_common.py +++ b/pylti/tests/test_common.py @@ -8,11 +8,10 @@ import httpretty import oauthlib.oauth1 -from six.moves.urllib.parse import urlencode, urlparse, parse_qs +from urllib.parse import urlencode, urlparse, parse_qs import pylti from pylti.common import ( - LTIOAuthServer, verify_request_common, LTIException, post_message, @@ -86,36 +85,6 @@ def test_version(): """ semantic_version.Version(pylti.__version__) - def test_lti_oauth_server(self): - """ - Tests that LTIOAuthServer works - """ - consumers = { - "key1": {"secret": "secret1"}, - "key2": {"secret": "secret2"}, - "key3": {"secret": "secret3"}, - "keyNS": {"test": "test"}, - "keyWCert": {"secret": "secret", "cert": "cert"}, - } - store = LTIOAuthServer(consumers) - self.assertEqual(store.lookup_consumer("key1").secret, "secret1") - self.assertEqual(store.lookup_consumer("key2").secret, "secret2") - self.assertEqual(store.lookup_consumer("key3").secret, "secret3") - self.assertEqual(store.lookup_cert("keyWCert"), "cert") - self.assertIsNone(store.lookup_consumer("key4")) - self.assertIsNone(store.lookup_cert("key4")) - self.assertIsNone(store.lookup_consumer("keyNS")) - self.assertIsNone(store.lookup_cert("keyNS")) - - def test_lti_oauth_server_no_consumers(self): - """ - If consumers are not given it there are no consumer to return. - """ - - store = LTIOAuthServer(None) - self.assertIsNone(store.lookup_consumer("key1")) - self.assertIsNone(store.lookup_cert("key1")) - def test_verify_request_common(self): """ verify_request_common succeeds on valid request @@ -134,7 +103,7 @@ def test_verify_request_common_via_proxy(self): """ headers = dict() headers['X-Forwarded-Proto'] = 'https' - orig_url = 'https://localhost:5000/?' + orig_url = 'https://localhost:5000/' consumers, method, url, verify_params, _ = ( self.generate_oauth_request(url_to_sign=orig_url) ) @@ -150,7 +119,7 @@ def test_verify_request_common_via_proxy_wsgi_syntax(self): """ headers = dict() headers['HTTP_X_FORWARDED_PROTO'] = 'https' - orig_url = 'https://localhost:5000/?' + orig_url = 'https://localhost:5000/' consumers, method, url, verify_params, _ = ( self.generate_oauth_request(url_to_sign=orig_url) ) @@ -177,7 +146,7 @@ def test_verify_request_common_no_params(self): consumers = { "__consumer_key__": {"secret": "__lti_secret__"} } - url = 'http://localhost:5000/?' + url = 'https://localhost:5000/' method = 'GET' headers = dict() params = dict() @@ -272,7 +241,7 @@ def generate_oauth_request(url_to_sign=None): consumers = { "__consumer_key__": {"secret": "__lti_secret__"} } - url = url_to_sign or 'http://localhost:5000/?' + url = url_to_sign or 'https://localhost:5000/' method = 'GET' params = {'resource_link_id': u'edge.edx.org-i4x-MITx-ODL_ENG-' u'lti-94173d3e79d145fd8ec2e83f15836ac8', @@ -300,7 +269,7 @@ def generate_oauth_request(url_to_sign=None): SIGNATURE_HMAC, signature_type=oauthlib.oauth1. SIGNATURE_TYPE_QUERY) - signature = client.sign("{}{}".format(url, urlparams)) + signature = client.sign("{}?{}".format(url, urlparams)) url_parts = urlparse(signature[0]) query_string = parse_qs(url_parts.query, keep_blank_values=True) diff --git a/pylti/tests/test_flask.py b/pylti/tests/test_flask.py index 4a50e09..c710ceb 100644 --- a/pylti/tests/test_flask.py +++ b/pylti/tests/test_flask.py @@ -9,7 +9,7 @@ import mock import oauthlib.oauth1 -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode from pylti.common import LTIException from pylti.flask import LTI @@ -127,7 +127,7 @@ def test_access_to_oauth_resource_without_authorization_initial_get(self): self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) self.assertEqual(self.get_exception_as_string(), - 'This page requires a valid oauth session or request') + 'OAuth error: Error while validating request.') def test_access_to_oauth_resource_without_authorization_initial_post(self): """ @@ -137,7 +137,7 @@ def test_access_to_oauth_resource_without_authorization_initial_post(self): self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) self.assertEqual(self.get_exception_as_string(), - 'This page requires a valid oauth session or request') + 'OAuth error: Error while validating request.') def test_access_to_oauth_resource_in_session(self): """ @@ -169,7 +169,7 @@ def test_access_to_oauth_resource(self): Accessing oauth_resource. """ consumers = self.consumers - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) self.app.get(new_url) @@ -181,7 +181,7 @@ def test_access_to_oauth_resource_name_passed(self): """ # pylint: disable=maybe-no-member consumers = self.consumers - url = 'http://localhost/name?' + url = 'https://localhost/name' add_params = {u'lis_person_sourcedid': u'person'} new_url = self.generate_launch_request( consumers, url, add_params=add_params @@ -197,7 +197,7 @@ def test_access_to_oauth_resource_email_passed(self): """ # pylint: disable=maybe-no-member consumers = self.consumers - url = 'http://localhost/name?' + url = 'https://localhost/name' add_params = {u'lis_person_contact_email_primary': u'email@email.com'} new_url = self.generate_launch_request( consumers, url, add_params=add_params @@ -213,7 +213,7 @@ def test_access_to_oauth_resource_name_and_email_passed(self): """ # pylint: disable=maybe-no-member consumers = self.consumers - url = 'http://localhost/name?' + url = 'https://localhost/name' add_params = {u'lis_person_sourcedid': u'person', u'lis_person_contact_email_primary': u'email@email.com'} new_url = self.generate_launch_request( @@ -229,7 +229,7 @@ def test_access_to_oauth_resource_staff_only_as_student(self): Deny access if user not in role. """ consumers = self.consumers - url = 'http://localhost/initial_staff?' + url = 'https://localhost/initial_staff' student_url = self.generate_launch_request( consumers, url, roles='Student' ) @@ -247,7 +247,7 @@ def test_access_to_oauth_resource_staff_only_as_administrator(self): Allow access if user in role. """ consumers = self.consumers - url = 'http://localhost/initial_staff?' + url = 'https://localhost/initial_staff' new_url = self.generate_launch_request( consumers, url, roles='Administrator' ) @@ -260,7 +260,7 @@ def test_access_to_oauth_resource_staff_only_as_unknown_role(self): Deny access if role not defined. """ consumers = self.consumers - url = 'http://localhost/initial_staff?' + url = 'https://localhost/initial_staff' admin_url = self.generate_launch_request( consumers, url, roles='Foo' ) @@ -273,7 +273,7 @@ def test_access_to_oauth_resource_student_as_student(self): Verify that the various roles we consider as students are students. """ consumers = self.consumers - url = 'http://localhost/initial_student?' + url = 'https://localhost/initial_student' # Learner Role learner_url = self.generate_launch_request( @@ -291,7 +291,7 @@ def test_access_to_oauth_resource_student_as_student(self): def test_access_to_oauth_resource_student_as_staff(self): """Verify staff doesn't have access to student only.""" consumers = self.consumers - url = 'http://localhost/initial_student?' + url = 'https://localhost/initial_student' staff_url = self.generate_launch_request( consumers, url, roles='Instructor' ) @@ -301,7 +301,7 @@ def test_access_to_oauth_resource_student_as_staff(self): def test_access_to_oauth_resource_student_as_unknown(self): """Verify staff doesn't have access to student only.""" consumers = self.consumers - url = 'http://localhost/initial_student?' + url = 'https://localhost/initial_student' unknown_url = self.generate_launch_request( consumers, url, roles='FooBar' ) @@ -360,16 +360,15 @@ def generate_launch_request(consumers, url, SIGNATURE_HMAC, signature_type=oauthlib.oauth1. SIGNATURE_TYPE_QUERY) - signature = client.sign("{}{}".format(url, urlparams)) + signature = client.sign("{}?{}".format(url, urlparams)) signed_url = signature[0] - new_url = signed_url[len('http://localhost'):] - return new_url + return signed_url def test_access_to_oauth_resource_any(self): """ Test access to LTI protected resources. """ - url = 'http://localhost/any?' + url = 'https://localhost/any' new_url = self.generate_launch_request(self.consumers, url) self.app.post(new_url) self.assertFalse(self.has_exception()) @@ -378,7 +377,7 @@ def test_access_to_oauth_resource_any_norole(self): """ Test access to LTI protected resources. """ - url = 'http://localhost/any?' + url = 'https://localhost/any' new_url = self.generate_launch_request(self.consumers, url, roles=None) self.app.post(new_url) self.assertFalse(self.has_exception()) @@ -387,7 +386,7 @@ def test_access_to_oauth_resource_any_nonstandard_role(self): """ Test access to LTI protected resources. """ - url = 'http://localhost/any?' + url = 'https://localhost/any' new_url = self.generate_launch_request(self.consumers, url, roles=u'ThisIsNotAStandardRole') self.app.post(new_url) @@ -398,13 +397,13 @@ def test_access_to_oauth_resource_invalid(self): Deny access to LTI protected resources on man in the middle attack. """ - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(self.consumers, url) self.app.get("{}&FAIL=TRUE".format(new_url)) self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) - self.assertEqual(self.get_exception_as_string(), + self.assertEqual(self.get_exception_as_string()[:45], 'OAuth error: Please check your key and secret') def test_access_to_oauth_resource_invalid_after_session_setup(self): @@ -415,13 +414,13 @@ def test_access_to_oauth_resource_invalid_after_session_setup(self): self.app.get('/session') self.assertFalse(self.has_exception()) - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(self.consumers, url) self.app.get("{}&FAIL=TRUE".format(new_url)) self.assertTrue(self.has_exception()) self.assertIsInstance(self.get_exception(), LTIException) - self.assertEqual(self.get_exception_as_string(), + self.assertEqual(self.get_exception_as_string()[:45], 'OAuth error: Please check your key and secret') @httpretty.activate @@ -438,7 +437,7 @@ def test_access_to_oauth_resource_post_grade(self): httpretty.register_uri(httpretty.POST, uri, body=self.request_callback) consumers = self.consumers - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) ret = self.app.get(new_url) @@ -473,7 +472,7 @@ def request_callback(request, cburi, headers): httpretty.register_uri(httpretty.POST, uri, body=request_callback) consumers = self.consumers - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) ret = self.app.get(new_url) self.assertFalse(self.has_exception()) @@ -493,7 +492,7 @@ def test_access_to_oauth_resource_post_grade_fix_url(self): httpretty.register_uri(httpretty.POST, uri, body=self.request_callback) - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request( self.consumers, url, lit_outcome_service_url=uri ) @@ -522,7 +521,7 @@ def test_access_to_oauth_resource_post_grade2(self): httpretty.register_uri(httpretty.PUT, uri, body=self.request_callback) consumers = self.consumers - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) ret = self.app.get(new_url) @@ -564,7 +563,7 @@ def request_callback(request, cburi, headers): httpretty.register_uri(httpretty.PUT, uri, body=request_callback) consumers = self.consumers - url = 'http://localhost/initial?' + url = 'https://localhost/initial' new_url = self.generate_launch_request(consumers, url) ret = self.app.get(new_url) @@ -588,7 +587,7 @@ def test_default_decorator(self): """ Verify default decorator works. """ - url = 'http://localhost/default_lti?' + url = 'https://localhost/default_lti' new_url = self.generate_launch_request(self.consumers, url) self.app.get(new_url) self.assertFalse(self.has_exception()) diff --git a/pytest.ini b/pytest.ini index dc256a8..221b395 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = pylti -xvs -rs --pep8 --flakes +addopts = pylti -xvs -rs --flakes # addopts = pylti -vs -rs --flakes diff --git a/requirements.txt b/requirements.txt index 0b87019..b002608 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ -Flask==0.10.1 -Chalice==1.3.0 -httpretty==0.8.3 -oauth2==1.9.0.post1 -oauthlib==2.0.6 -pyflakes==1.2.3 -pytest==2.9.2 -pytest-cache==1.0 -pytest-cov==2.3.0 -pytest-flakes==1.0.1 -pytest-pep8==1.0.6 -httplib2==0.9.2 -six==1.11.0 +Flask +Chalice +httpretty +oauthlib +pyflakes +pytest +pytest-cache +pytest-cov +pytest-flakes +requests-oauthlib +semantic-version +mock diff --git a/setup.cfg b/setup.cfg index 2a9acf1..526aeb2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [bdist_wheel] -universal = 1 +universal = 0 diff --git a/setup.py b/setup.py index 79498ba..22571e4 100644 --- a/setup.py +++ b/setup.py @@ -9,8 +9,8 @@ import os import sys -if sys.version_info < (2, 7): - error = "ERROR: PyLTI requires Python 2.7+ ... exiting." +if sys.version_info < (3, 7): + error = "ERROR: PyLTI requires Python 3.7+ ... exiting." print(error, file=sys.stderr) sys.exit(1) @@ -22,12 +22,10 @@ class PyTest(testcommand): user_options = testcommand.user_options[:] user_options += [ ('coverage', 'C', 'Produce a coverage report for PyLTI'), - ('pep8', 'P', 'Produce a pep8 report for PyLTI'), ('flakes', 'F', 'Produce a flakes report for PyLTI'), ] coverage = None - pep8 = None flakes = None test_suite = False test_args = [] @@ -42,8 +40,6 @@ def finalize_options(self): if self.coverage: self.test_args.append('--cov') self.test_args.append('pylti') - if self.pep8: - self.test_args.append('--pep8') if self.flakes: self.test_args.append('--flakes') @@ -54,13 +50,13 @@ def run_tests(self): sys.exit(errno) extra = dict(test_suite="pylti.tests", - tests_require=["pytest-cov>=2.3.0", "pytest-pep8>=1.0.6", - "pytest-flakes>=1.0.1", "pytest>=2.9.2", - "httpretty>=0.8.3", "flask>=0.10.1", - "oauthlib>=0.6.3", "semantic_version>=2.3.1", - "mock==1.0.1"], + tests_require=["pytest-cov==4.*", + "pytest-flakes==4.*", "pytest==7.*", + "httpretty==1.*", "chalice==1.*", "flask==2.*", + "oauthlib==3.*", "semantic_version==2.*", + "mock==5.*"], cmdclass={"test": PyTest}, - install_requires=["oauth2>=1.9.0.post1", "httplib2>=0.9", "six>=1.10.0"], + install_requires=["oauthlib==3.*", "requests-oauthlib==1.*"], include_package_data=True, zip_safe=False) except ImportError as err: diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index d515c2c..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -pytest-cov>=2.3.0 -pytest-pep8>=1.0.6 -pytest-flakes>=1.0.1 -pytest>=2.9.2 -httpretty>=0.8.3 -flask>=0.10.1 -chalice>=1.3.0 -oauthlib>=0.6.3 -semantic_version>=2.3.1 -mock>=1.0.1 -oauth2>=1.9.0.post1 -six>=1.11.0