def unquote_plus(v): OAUTH_PARAM_TOKEN = "oauth_token" OAUTH_PARAM_SIGNATURE_METHOD = "oauth_signature_method" OAUTH_PARAM_TIMESTAMP = "oauth_timestamp" OAUTH_PARAM_NONCE = "oauth_nonce" OAUTH_PARAM_SIGNATURE = "oauth_signature" OAUTH_PARAM_TOKEN_SECRET = "oauth_token_secret" OAUTH_PARAM_CALLBACK = "oauth_callback" OAUTH_PARAM_CONSUMER_SECRET = "oauth_consumer_secret" OAUTH_PARAM_VERIFIER = "oauth_verifier" OAUTH_PARAM_VERSION = "oauth_version" OAUTH_PARAM_CONSUMER_KEY = "oauth_consumer_key" class InvalidUrlError(OAuthError):
    """
    Raised when a specified URL is invalid for consumption by a routine.
    """
    pass class InvalidQueryParametersError(OAuthError):
    """
    Raised when a query parameter is invalid.
    """
    pass class InsecureOAuthUrlError(OAuthError):
    """
    Raised when an insecure (non-HTTPS) URL is detected for requesting OAuth
    credentials.
    """
    pass class InvalidOAuthParametersError(OAuthError):
    """
    Raised when invalid protocol parameters are detected.
    """
    pass class InsecureOAuthParametersError(OAuthError):
    """
    Raised when an OAuth confidential parameter is passed into protocol
    parameters.
    """
    pass def percent_decode(value):
    """
    Percent-decodes according to the OAuth spec::

        percent_decode('c%40') -> 'c@'

    :see: Percent Encoding (http://tools.ietf.org/html/rfc5849#section-3.6)
    :param value:
        Value to percent-decode. '+' is treated as a ' ' character.
    :returns:
        Percent-decoded value.
    """
    return utf8_decode_if_bytes(unquote_plus(value)) def percent_encode(value):
    """
    Percent-encodes according to the OAuth spec.

    Used in constructing the signature base string and the "Authorization"
    header field::

        percent_encode('c@') -> 'c%40'

    :see: Percent Encoding (http://tools.ietf.org/html/rfc5849#section-3.6)
    :param value:
        Query string parameter value to escape. If the value is a Unicode
        string, it will be encoded to UTF-8. A byte string is considered
        exactly that, a byte string and will not be UTF-8 encoded—however, it
        will be percent-encoded.
    :returns:
        Percent-encoded string.
   """
    # Escapes '/' too
    if not is_bytes(value):
        value = utf8_encode(str(value))
    return quote(value, safe="~").encode("ascii") def parse_qs(query_string):
    """
    Parses a query parameter string according to the OAuth spec.

    Use only with OAuth query strings.

    :see: Parameter Sources
        (http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1)
    :param query_string:
        Query string to parse. If ``query_string`` starts with a ``?`` character
        it will be ignored for convenience.
    """
    query_string = utf8_encode_if_unicode(query_string) or SYMBOL_EMPTY_BYTES
    if query_string.startswith(SYMBOL_QUESTION_MARK):
        logging.warning(
            "Ignoring `?` query string prefix -- `%r`", query_string)
        query_string = query_string[1:]
    return _parse_qs(query_string, keep_blank_values=True) def urlencode_s(query_params, predicate=None):
    """
    Serializes a dictionary of query parameters into a string of query
    parameters, ``name=value`` pairs separated by ``&``, sorted first by
    ``name`` then by ``value`` based on the OAuth percent-encoding
    rules and specification.

    Behaves like :func:`urllib.urlencode` with ``doseq=1``.

    :param query_params:
        Dictionary of query parameters.
    :param predicate:
        A callback that will be called for each query parameter and should
        return ``False`` or a falsy value if that parameter should not be
        included. By default, all query parameters are included. The function
        takes the following method signature::

            def predicate(name, value):
                return is_name_allowed(name) and is_value_allowed(value)
    :returns:
        A string of query parameters, ``name=value`` pairs separated by ``&``,
        sorted first by ``name`` and then by ``value`` based on the OAuth
        percent-encoding rules and specification.
    """
    return SYMBOL_AMPERSAND.join(
        key + SYMBOL_EQUAL + value
        for key, value in urlencode_sl(query_params, predicate)) def urlencode_sl(query_params, predicate=None):
    """
    Serializes a dictionary of query parameters into a list of query
    parameters, ``(name, value)`` pairs, sorted first by ``name`` then by
    ``value`` based on the OAuth percent-encoding rules and specification.

    Behaves like :func:`urllib.urlencode` with ``doseq=1``.

    :param query_params:
        Dictionary of query parameters.
    :param predicate:
        A callback that will be called for each query parameter and should
        return ``False`` or a falsy value if that parameter should not be
        included. By default, all query parameters are included. The function
        takes the following method signature::

            def predicate(name, value):
                return is_name_allowed(name) and is_value_allowed(value)
    :returns:
        A list of query parameters, ``(name, value)`` pairs, sorted first by
        ``name`` and then by ``value`` based on the OAuth percent-encoding rules
        and specification.
    """
    query_params = query_params or {}
    encoded_pairs = []
    for k, value in query_params.items():
        # Keys are also percent-encoded according to OAuth spec.
        key = percent_encode(k)
        if predicate and not predicate(k, value):
            continue
        elif is_bytes_or_unicode(value):
            encoded_pairs.append((key, percent_encode(value),))
        elif is_sequence(value):
            # Loop over the sequence.
            if len(value) > 0:
                for i in value:
                    encoded_pairs.append((key, percent_encode(i), ))
            # ``urllib.urlencode()`` doesn't preserve blank lists.
            # Therefore, we're discarding them.
            #else:
            #    # Preserve blank list values.
            #    encoded_pairs.append((k, "", ))
        else:
            encoded_pairs.append((key, percent_encode(value),))
    # Sort after encoding according to the OAuth spec.
    return sorted(encoded_pairs) def query_unflatten(query):
    """
    Given a query string parses it into an un-flattened query parameter
    dictionary or given a parameter dictionary, un-flattens it.

    Example::

        dict(a=1, b=[1, 2], c="")   ->   dict(a[1], b=[1, 2], c=[""])
        a=1&b=1&b=2&c=              ->   dict(a[1], b=[1, 2], c=[""])

    :param query:
        A query parameter dictionary or a query string.
        If this argument is ``None`` an empty dictionary will be returned.
        Any other value will raise a
        :class:`pyoauth.errors.InvalidQueryParametersError` exception.
    :returns:
        An un-flattened query parameter dictionary.
    """
    if is_bytes_or_unicode(query):
        return parse_qs(query)
    elif isinstance(query, dict):
        # Un-flatten the dictionary.
        def _choose(key, value):
            if not isinstance(value, list) and not isinstance(value, tuple):
                return key, [value]
            else:
                return key, list(value)
        return map_dict(_choose, query)
        # Alternative, but slower:
        #return parse_qs(urlencode_s(query))
    elif query is None:
        return {}
    else:
        raise InvalidQueryParametersError(
            "Dictionary or query string required: got `%r` instead" \
            % (query, )) def query_add(*queries):
    """
    Merges multiple query parameter dictionaries or strings.

    :param queries:
        One or more query string or a dictionary of query parameters.
    :returns:
        A dictionary of merged query parameters.
    """
    new_query_d = {}
    for query in queries:
        query_d = query_unflatten(query)
        for name, value in query_d.items():
            if name in new_query_d:
                new_query_d[name].extend(value)
            else:
                new_query_d[name] = value
    return new_query_d def urlparse_normalized(url):
    """
    Like :func:`urlparse.urlparse` but also normalizes scheme, netloc, port,
    and the path.

    Use with OAuth URLs.

    :see: Base String URI (http://tools.ietf.org/html/rfc5849#section-3.4.1.2)
    :param url:
        The URL to split and normalize.
    :returns:
        Tuple that contains these elements:
        ``(scheme, netloc, path, params, query, fragment)``
    """
    if not url:
        raise InvalidUrlError("Invalid URL `%r`" % (url,))

    parts = urlparse(url)

    scheme      = parts.scheme.lower()
    # Netloc.
    username    = parts.username or SYMBOL_EMPTY_BYTES
    password    = (b(":") + parts.password) if parts.password \
                  else SYMBOL_EMPTY_BYTES
    credentials = username + password
    credentials = (credentials + b("@")) if credentials else SYMBOL_EMPTY_BYTES
    hostname = utf8_encode_if_unicode(parts.hostname.lower())

    # Exclude default port numbers.
    # See:
    if parts.port:
        if (scheme == b("http") and parts.port == 80) \
        or (scheme == b("https") and parts.port == 443):
            port = SYMBOL_EMPTY_BYTES
        else:
            port = (b(":") + str(parts.port).encode("ascii")) if parts.port \
                   else SYMBOL_EMPTY_BYTES
    else:
        port = SYMBOL_EMPTY_BYTES

    netloc        = credentials + hostname + port
    # http://tools.ietf.org/html/rfc3986#section-3
    # and http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2.2
    path          = parts.path or b("/")
    matrix_params = parts.params or SYMBOL_EMPTY_BYTES
    fragment      = parts.fragment or SYMBOL_EMPTY_BYTES
    query         = parts.query or SYMBOL_EMPTY_BYTES

    return scheme, netloc, path, matrix_params, query, fragment def url_add_query(url, query, predicate=None):
    """
    Adds additional query parameters to a URL while preserving existing ones.

    The URL will be normalized according to the OAuth specification with the
    exception that the URL fragment is preserved.

    :param url:
        The URL to add the additional query parameters to.
    :param query:
        The additional query parameters as a dictionary object or a query
        string.
    :param predicate:
        A callback that will be called for each query parameter and should
        return ``False`` or a falsy value if that parameter should not be
        included. By default, all query parameters are included. The function
        takes the following method signature::

            def predicate(name, value):
                return is_name_allowed(name) and is_value_allowed(value)
    :returns:
        A normalized URL with the fragment and existing query parameters
        preserved and with the extra query parameters added.
    """
    scheme, netloc, path, params, query_s, fragment = urlparse_normalized(url)

    query_d = query_add(query_s, query)
    query_s = urlencode_s(query_d, predicate)
    return urlunparse((scheme, netloc, path, params, query_s, fragment)) def query_remove_oauth(query):
    """
    Removes protocol parameters from the query parameters.

    Used only in base string construction, Authorization headers construction
    and parsing, and OAuth requests.

    :param query:
        Query string or query parameter dictionary.
    :returns:
        Filtered URL query parameter dictionary.
    """
    def predicate(name, _):
        """
        Removes any parameters beginning with ``oauth_``.

        :param name:
            The parameter name.
        :returns:
            ``True`` if should be included; ``False`` otherwise.
        """
        # This gets rid of any params beginning with "oauth_"
        name = utf8_decode_if_bytes(name)
        if not name.startswith(OAUTH_PARAM_PREFIX):
            return True
        else:
            logging.warning(
                "Protocol parameter ignored from URL query parameters: `%r`",
                name)
            return False
    return query_select(query, predicate) def request_query_remove_non_oauth(query):
    """
    Removes non-OAuth and non-transmittable OAuth parameters from the
    request query parameters.

    .. WARNING:: Do NOT use this function with responses.

        Use ONLY with requests.

        Specifically used ONLY in base string construction, Authorization
        headers construction and parsing, and OAuth requests.

    :param query:
        Query string or query parameter dictionary. Does not filter out
        ``oauth_signature``, but DOES filter out ``oauth_consumer_secret`` and
        ``oauth_token_secret``. These secret parameters must never be
        transmitted.
    :returns:
        Filtered protocol parameters dictionary.
    """
    def predicate(name, value):
        """
        Allows only valid oauth parameters.

        Raises an error if multiple values are specified.

        :param name:
            Protocol parameter name.
        :param value:
            Protocol parameter value.
        :returns:
            ``True`` if the parameter should be included; ``False`` otherwise.
        """
        name = utf8_decode_if_bytes(name)
        if name.startswith(OAUTH_PARAM_PREFIX):
            # This gets rid of "realm" or any non-OAuth param.
            if len(value) > 1:
                # Multiple values for a protocol parameter are not allowed.
                # We don't silently discard values because failing fast
                # is better than simply logging and waiting for the user
                # to figure it out all by herself.
                #
                # See Making Requests
                # (http://tools.ietf.org/html/rfc5849#section-3.1)
                # Point 2. Each parameter MUST NOT appear more than once per
                # request, so we disallow multiple values for a protocol
                # parameter.
                raise InvalidOAuthParametersError(
                    "Multiple protocol parameter values found %r=%r" \
                    % (name, value))
            elif name in (OAUTH_PARAM_CONSUMER_SECRET,
                          OAUTH_PARAM_TOKEN_SECRET, ):
                raise InsecureOAuthParametersError(
                    "[SECURITY-ISSUE] Client attempting to transmit "\
                    "confidential protocol parameter `%r`. Communication "\
                    "is insecure if this is in your server logs." % (name, ))
            else:
                return True
        else:
            logging.warning("Invalid protocol parameter ignored: `%r`", name)
            return False
    return query_select(query, predicate) def oauth_url_sanitize(url, force_secure=True):
    """
    Normalizes an OAuth URL and cleans up protocol-specific parameters
    from the query string.

    Used only in base string construction, Authorization headers construction
    and parsing, and OAuth requests.

    :param url:
        The OAuth URL to sanitize.
    :returns:
        Normalized sanitized URL.
    """
    scheme, netloc, path, params, query, _ = urlparse_normalized(url)
    query = urlencode_s(query_remove_oauth(query))
    if force_secure and scheme != b("https"):
        raise InsecureOAuthUrlError(
            "OAuth specification requires the use of SSL/TLS for "\
            "inter-server communication.")
    elif not force_secure and scheme != b("https"):
        logging.warning(
            "INSECURE URL: OAuth specification requires the use of SSL/TLS "\
            "for credential requests.")
    return urlunparse((scheme, netloc, path, params, query, None)) def url_append_query(url, query):
    """
    Appends query params to any existing query string in the URL
    and returns a properly formatted URL. URL fragments are preserved.

    This is the equivalent of doing::

        sorted(URL query parameters) + "&" + sorted(query)

    :param url:
        The URL into which the query parameters will be concatenated.
    :param query:
        A dictionary of query parameters or a query string.
    :returns:
        A URL with the query parameters concatenated.

    Usage::

        >>> url_append_query("http://example.com/foo?a=b#fragment", dict(c="d"))
        'http://example.com/foo?a=b&c=d#fragment'
    """
    if not query:
        return url
    scheme, netloc, path, params, query_s, fragment = urlparse_normalized(url)
    query_s = (query_s + SYMBOL_AMPERSAND) if query_s else query_s
    query_s = query_s + urlencode_s(query_unflatten(query))
    return urlunparse((scheme, netloc, path, params, query_s, fragment)) def query_append(*queries):
    """
    Appends additional query parameters to a query string. The additional
    query parameters appear after the initial query string.

    :param queries:
        Additional query parameters dictionary or query string.
    :returns:
        Concatenated query string.
    """
    sub_queries = []
    for query in queries:
        query_s = urlencode_s(query_unflatten(query))
        if query_s:
            sub_queries.append(query_s)
    return SYMBOL_AMPERSAND.join(sub_queries) def is_valid_callback_url(url):
    """
    Determines whether a specified URL is a valid oauth_callback callback
    absolute URL as required by http://tools.ietf.org/html/rfc5849#section-2.1
    (Temporary Credentials) in the OAuth specification.

    :param url:
        The URL to validate.
    :returns:
        ``True`` if valid; ``False`` otherwise.
    """
    if not is_bytes(url):
        return False
    if url == OAUTH_VALUE_CALLBACK_OOB:
        return True
    else:
        scheme, netloc, _, _, _, _ = urlparse(url)
        return scheme.lower() in (b("http"), b("https")) and netloc TEST_CONSUMER_KEY = b("consumer-key")
TEST_NONCE = b("nonce")
TEST_TIMESTAMP = b("timestamp")
TEST_IGNORE_THIS_TEXT = b("ignore-this")
TEST_TOKEN = b("token")
TEST_EXTRA_PARAM_VALUE = b("extra-parameter")
RFC_CLIENT_IDENTIFIER = b("dpf43f3p2l4k3l03")
RFC_CLIENT_SECRET = b("kd94hf93k423kf44")
RFC_TEMPORARY_IDENTIFIER = b("hh5s93j4hdidpola")
RFC_TEMPORARY_SECRET = b("hdhd0244k9j7ao03")
RFC_TOKEN_IDENTIFIER = b("nnch734d00sl2jdk")
RFC_TOKEN_SECRET = b("pfkkdhi9sl3r4s00")
RFC_OAUTH_VERIFIER = b("hfdp7dh39dks9884")
BAD_RFC_TEMP_CREDENTIALS_RESPONSE = b("oauth_token=hh5s93j4hdidpola\
&oauth_token_secret=hdhd0244k9j7ao03")
RFC_TEMP_CREDENTIALS_RESPONSE = b("oauth_token=hh5s93j4hdidpola\
&oauth_token_secret=hdhd0244k9j7ao03&oauth_callback_confirmed=true")
RFC_TOKEN_CREDENTIALS_RESPONSE = b("oauth_token=nnch734d00sl2jdk\
&oauth_token_secret=pfkkdhi9sl3r4s00")
RFC_TEMP_URI = b("https://photos.example.net/initiate")
RFC_TOKEN_URI = b("https://photos.example.net/token")
RFC_AUTHORIZATION_URI = b("https://photos.example.net/authorize")
RFC_AUTHENTICATION_URI = b("https://photos.example.net/authenticate")
RFC_OAUTH_CALLBACK_URI = b("http://printer.example.com/ready")
RFC_RESOURCE_URI = b("http://photos.example.net/photos")
RFC_RESOURCE_FULL_URL = \
    b("http://photos.example.net/photos?file=vacation.jpg&size=original")
FOO_URI = b("http://example.com/foo")
BAD_SIGNATURE_METHOD = b("HMAC-SHOOOOOO1")
BAD_SIGNATURE = b("BOOM!")
BAD_OAUTH_CALLBACK = b("foobar")
BAD_CREDENTIALS_CONTENT_TYPE = b('INVALID-CONTENT-TYPE')
RFC_TIMESTAMP_1 = b("137131200")
RFC_TIMESTAMP_2 = b("137131201")
RFC_TIMESTAMP_3 = b("137131202")
RFC_NONCE_1 = b("wIjqoS")
RFC_NONCE_2 = b("walatlh")
RFC_NONCE_3 = b("chapoH")
RFC_REALM = b("Photos")
RFC_TEMP_REQUEST_SIGNATURE = b("74KNZJeDHnMBp0EMJ9ZHt/XKycU=")
RFC_TOKEN_REQUEST_SIGNATURE = b("gKgrFCywp7rO0OXSjdot/IHF7IU=")
RFC_RESOURCE_REQUEST_SIGNATURE = b("MdpQcU8iPSUjWoN/UDMsK2sui9I=")
RFC_RESOURCE_REQUEST_SIGNATURE_ENCODED = b("MdpQcU8iPSUjWoN%2FUDMsK2sui9I%3D")
CONTENT_TYPE_TEXT_CSS = b("text/css")
import logging
import unittest2
from mom.builtins import b, is_bytes
from mom.codec.text import utf8_encode_if_unicode, \
    utf8_decode_if_bytes, utf8_encode
from pyoauth._compat import urlparse
from pyoauth.constants import \
    OAUTH_PARAM_TOKEN, OAUTH_PARAM_SIGNATURE_METHOD, \
    OAUTH_PARAM_TIMESTAMP, OAUTH_PARAM_NONCE, OAUTH_PARAM_SIGNATURE, \
    OAUTH_PARAM_TOKEN_SECRET, OAUTH_PARAM_CALLBACK, \
    OAUTH_PARAM_CONSUMER_SECRET, OAUTH_PARAM_VERIFIER, OAUTH_PARAM_VERSION, \
    OAUTH_PARAM_CONSUMER_KEY
from pyoauth.error import InvalidUrlError, \
    InvalidQueryParametersError, \
    InsecureOAuthUrlError, \
    InvalidOAuthParametersError, \
    InsecureOAuthParametersError
from pyoauth.url import \
    percent_decode, \
    percent_encode, \
    parse_qs, \
    urlencode_s, \
    urlencode_sl, \
    query_unflatten, \
    query_add, \
    urlparse_normalized, \
    url_add_query, \
    query_remove_oauth, \
    request_query_remove_non_oauth, \
    oauth_url_sanitize, \
    url_append_query, \
    query_append, \
    is_valid_callback_url
from pyoauth.tests.constants import constants
                       b("["),
                       b("]"),
                       b("|"),
                       b("\\"),
                       b(":"),
                       b(";"),
                       b('"'),
                       b("'"),
                       b(","),
                       b("<"),
                       b(">"),
                       b("?"),
                       b("/"),
                       b("`"),
                       b("´"),
                       constants.test_unicode_angstrom,
                       ]

#    def test_percent_encoded_unicode_input(self):
#        self.assertEqual(percent_decode(b("%FF%FE%E5%00%E9%00%EE%00%F8%00%FC%00")),
#                     constants.test_unicode_aeiou.encode("utf-16"))

    def test_plus_is_treated_as_space_character(self):
        self.assertEqual(
            percent_decode(b('+')), " ",
            "Plus character in encoding is not treated as space character.")

    def test_oauth_test_cases(self):
        # http://wiki.oauth.net/w/page/12238556/TestCases
        ex = constants.percent_decode_test_cases
        for decoded, encoded in ex:
            self.assertEqual(percent_decode(encoded), decoded)

            
class Test_urlencode_s(unittest2.TestCase):
    def test_valid_query_string(self):
        params = {
            "a2": "r b",
            "b5": "=%3D",
            "a3": ["a", "2 q"],
            "c@": [""],
            "c2": "",
            OAUTH_PARAM_CONSUMER_KEY: "9djdj82h48djs9d2",
            OAUTH_PARAM_TOKEN: "kkk9d7dh3k39sjv7",
            OAUTH_PARAM_SIGNATURE_METHOD: "HMAC-SHA1",
            OAUTH_PARAM_TIMESTAMP: ["137131201"],
            OAUTH_PARAM_NONCE: "7d8f3e4a",
        }
        valid_query_string = b("""\
a2=r%20b\
&a3=2%20q\
&a3=a\
&b5=%3D%253D\
&c%40=\
&c2=\
&oauth_consumer_key=9djdj82h48djs9d2\
&oauth_nonce=7d8f3e4a\
&oauth_signature_method=HMAC-SHA1\
&oauth_timestamp=137131201\
&oauth_token=kkk9d7dh3k39sjv7""")
        self.assertEqual(urlencode_s(params), valid_query_string)

    def test_do_seq_dicts(self):
        # Behaves like doseq=1
        params = dict(a=dict(a='b'), c="something")
        self.assertEqual(urlencode_s(params), b('a=a&c=something'))

    def test_do_seq_removes_blank_lists(self):
        params = dict(a=[], c="something")
        self.assertEqual(urlencode_s(params), b("c=something"))


class Test_urlencode_sl(unittest2.TestCase):
    def test_valid_query_params_list(self):
        params = {
            "a2": "r b",
            "b5": "=%3D",
            "a3": ["a", "2 q"],
            "c@": [""],
            "c2": "",
            "non_string": 5,
            "blank_list_value_not_preserved": [],
            OAUTH_PARAM_CONSUMER_KEY: "9djdj82h48djs9d2",
            OAUTH_PARAM_TOKEN: "kkk9d7dh3k39sjv7",
            OAUTH_PARAM_SIGNATURE_METHOD: "HMAC-SHA1",
            OAUTH_PARAM_TIMESTAMP: ["137131201"],
            OAUTH_PARAM_NONCE: "7d8f3e4a",
        }
        valid_params_list = [
            (b("a2"), b("r%20b")),
            (b("a3"), b("2%20q")),
            (b("a3"), b("a")),
            (b("b5"), b("%3D%253D")),
            (b("c%40"), b("")),
            (b("c2"), b("")),
            (b("non_string"), b("5")),
            (utf8_encode(OAUTH_PARAM_CONSUMER_KEY), b("9djdj82h48djs9d2")),
            (utf8_encode(OAUTH_PARAM_NONCE), b("7d8f3e4a")),
            (utf8_encode(OAUTH_PARAM_SIGNATURE_METHOD), b("HMAC-SHA1")),
            (utf8_encode(OAUTH_PARAM_TIMESTAMP), b("137131201")),
            (utf8_encode(OAUTH_PARAM_TOKEN), b("kkk9d7dh3k39sjv7")),
        ]
        self.assertEqual(urlencode_sl(params), valid_params_list)

    def test_blank_list_value_not_preserved(self):
        params = {
            "blank_list_value_not_preserved": [],
            OAUTH_PARAM_CONSUMER_KEY: "9djdj82h48djs9d2",
            OAUTH_PARAM_TOKEN: "kkk9d7dh3k39sjv7",
            OAUTH_PARAM_SIGNATURE_METHOD: "HMAC-SHA1",
            OAUTH_PARAM_TIMESTAMP: ["137131201"],
            OAUTH_PARAM_NONCE: "7d8f3e4a",
        }
        valid_params_list = [
            (utf8_encode(OAUTH_PARAM_CONSUMER_KEY), b("9djdj82h48djs9d2")),
            (utf8_encode(OAUTH_PARAM_NONCE), b("7d8f3e4a")),
            (utf8_encode(OAUTH_PARAM_SIGNATURE_METHOD), b("HMAC-SHA1")),
            (utf8_encode(OAUTH_PARAM_TIMESTAMP), b("137131201")),
            (utf8_encode(OAUTH_PARAM_TOKEN), b("kkk9d7dh3k39sjv7")),
        ]

        self.assertEqual(urlencode_sl(params), valid_params_list)