. "$BST_LIB" # BST-TIMEOUT: 2000 # FIVE python3 starts, and they are the whole cost. Four are # # one-line imports -- `import oauthlib', the oauth1 package, # # the oauth2 package, and a bare import whose STDERR is the # # assertion (`import-quiet') -- and the fifth is the probe, # # which takes ONE start for 89 measurements. # # # # The work inside the probe is HMAC-SHA1/SHA256 over a few # # hundred bytes and a dozen string normalisations, # # microseconds beside an interpreter start on this arm, # # which is tens of seconds before the first line of the # # program runs. Nothing in it sleeps, retries or waits. # # # # 2000 is 400s per start, the same per-start budget # # 99-python3-flexcache and 99-python3-dateutil set. # # # # A suite-timeout red is a batch-wide cost, so if the five # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-oauthlib 3.2.2 (meta-openembedded/meta-python # recipes-devtools/python/python3-oauthlib_3.2.2.bb, `inherit pypi setuptools3', # PyPI sdist) on the wasm32-hwjs glibc image. BRINTOS-2406. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # oauthlib is the SIGNING and GRANT logic of OAuth, and nothing else -- it is # not a client. It has no transport: it builds and parses strings, and the # caller does the HTTP. That is why every case below is a pure function of its # input, and why `no-socket-opened' is a meaningful thing to assert rather than # a formality (see the probe's guard, and the case of the same name). # # Two specifications, both in scope: # # OAuth 1.0a (RFC 5849) request signing. A canonical `signature base string' # is built from the method, the normalised URI and the # normalised parameters, and then signed. HMAC-SHA1, # HMAC-SHA256 and PLAINTEXT are available here; # RSA-SHA1 is NOT -- see the scope note below. # OAuth 2.0 (RFC 6749) authorisation-request URI construction, token-response # parsing, and the named error responses. # # ============================================================================ # WHAT IS OUT OF SCOPE, AND WHY THE SUITE ASSERTS ITS ABSENCE. # ============================================================================ # # The recipe's four PACKAGECONFIG extras (test, signedtoken, signals, rsa) are # ALL OFF, which is the recipe's own default -- its `PACKAGECONFIG ?=' line is # commented out upstream, so this is the absence of an override rather than one. # Each extra pulls python3-cryptography or python3-pyjwt, and # python3-cryptography `inherit cargo': Rust does not target wasm32-hwjs. # # So RSA-SHA1 is unavailable, and that is a CONTRACT rather than a gap to skip # quietly. oauthlib reaches PyJWT through an UNGUARDED, LAZY `import # jwt.algorithms' inside signature._get_jwt_rsa_algorithm(), so: # # `import oauthlib' and every HMAC/PLAINTEXT path SUCCEED, and # an RSA SIGNING CALL fails at CALL time with an ImportError subclass. # # `rsa-sha1-raises', `rsa-sha1-is-import-error' and `rsa-sha1-names-jwt' assert # exactly that shape. A suite that merely skipped RSA would pass just as well on # an image where oauthlib was broken outright. # # ============================================================================ # WHY THIS PORT NEEDED A BBAPPEND: python3-unittest IS DECLARED AND UNREACHED. # ============================================================================ # # meta-python's RDEPENDS names core, crypt, datetime, json, logging, math, # netclient and unittest. The first seven are each reached; `unittest' is not # reached on any Python, and appears NOWHERE in the shipped package -- it occurs # only under the sdist's `tests/' directory, which this recipe does not install. # The bbappend removes it; the full measurement (a sys.meta_path refusal per # split, against the tree from the BUILT .deb) is in the bbappend header. # # `pkg-depends-no-unittest' asserts the removal and `import-unittest-still-works' # asserts the split is still ON the image -- python3-crcmod is on this image and # its own RDEPENDS names python3-unittest (meta-openembedded # python3-crcmod_1.7.bb:12), so the removal cannot change this image's installed # set. Two cases rather than one because they fail for different reasons: # the first reds if the bbappend stopped taking, the second # if the split left the image, and a single case could not tell them apart. # # `stringold' and `email' are reached and are deliberately NOT declared: each # arrives through a split that IS declared (logging -> string, urllib.request -> # email), and a split reached through another one's closure is that one's to # name. Same rule as the python3-flexcache entry, applied in the other direction. # # ============================================================================ # WHERE THE EXPECTED VALUES COME FROM. # ============================================================================ # # The OAuth 1.0a values are the WORKED EXAMPLE of RFC 5849 section 3.4.1 -- the # base string from the end of 3.4.1.1, the normalised parameters from 3.4.1.3.2, # the parameter table from 3.4.1.3.1. They are the SPECIFICATION's own values, # so `base-matches-rfc' and `normalized-matches-rfc' are oauthlib being checked # against the RFC rather than against itself. # # Everything else is a literal measured on an x86-64 control (CPython 3.12.3) # against the package unpacked from the BUILT .deb, with PyJWT, cryptography and # blinker REFUSED through a sys.meta_path finder so the control has the target's # package set rather than the build host's. That refusal is not a detail: run # without it, the control has PyJWT installed and `rsa-sha1-raises' measures # ValueError from a dummy key instead of the ModuleNotFoundError this image # produces -- which would have shipped a case that could never go green here. # # The 68 shipped modules are BYTE-IDENTICAL to the pinned sdist (cmp, all 68), # and the package carries ZERO .so. This image's python3 is one static binary # with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a module # with a compiled extension could not be imported whatever it built to; # `pkg-no-so' and `tree-no-so' assert that negative directly. # # THE HMAC CASES ARE CROSS-CHECKED, NOT ONLY PINNED. RFC 5849 publishes an # example oauth_signature but NOT the secrets that produce it, so that value # cannot be recomputed by anyone. The probe therefore signs with its own secrets # and ALSO recomputes the digest straight from hashlib/hmac, and # `hmac-sha1-matches-stdlib' compares the two. A pinned literal on its own would # stay green on an arm where oauthlib and the literal were wrong the same way. # # ============================================================================ # NOT MEASURED ON TARGET AT THE TIME OF WRITING. # ============================================================================ # # Phase A of a batched Epic pass builds at recipe scope only and never boots an # image, so every expectation below is a control measurement carried forward, # not an on-target result. The batch verify is this port's first on-target run. # Nothing here is written as though it had already been observed on the guest. P=python3-oauthlib bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/oauthlib DI=$SITE/oauthlib-3.2.2.dist-info PKGLIST=/var/lib/dpkg/info/python3-oauthlib.list PROBE=$BST_DATA/python3-oauthlib-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-oauthlib.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ installed tree --- check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKGDIR" check $P/no-module-py test ! -f "$SITE/oauthlib.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/common-py test -s "$PKGDIR/common.py" check $P/signals-py test -s "$PKGDIR/signals.py" check $P/uri-validate-py test -s "$PKGDIR/uri_validate.py" # The four top-level modules, and the count so a fifth arriving is visible. check_eq $P/top-module-count "4" \ "$(find "$PKGDIR" -maxdepth 1 -name '*.py' 2>/dev/null | wc -l | tr -d ' ')" # 68 modules over 32 directories, measured from the built .deb. check_eq $P/module-count "68" \ "$(find "$PKGDIR" -name '*.py' 2>/dev/null | wc -l | tr -d ' ')" check_eq $P/dir-count "32" \ "$(find "$PKGDIR" -type d 2>/dev/null | wc -l | tr -d ' ')" # The three specification subtrees, each a package in its own right. check $P/oauth1-dir test -d "$PKGDIR/oauth1/rfc5849" check $P/oauth2-dir test -d "$PKGDIR/oauth2/rfc6749" check $P/rfc8628-dir test -d "$PKGDIR/oauth2/rfc8628" check $P/openid-dir test -d "$PKGDIR/openid/connect/core" check $P/signature-py test -s "$PKGDIR/oauth1/rfc5849/signature.py" check $P/parameters-py test -s "$PKGDIR/oauth2/rfc6749/parameters.py" check $P/errors-py test -s "$PKGDIR/oauth2/rfc6749/errors.py" check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-common test -s "$PKGDIR/__pycache__/common.cpython-312.pyc" # The sdist's tests/ tree is NOT installed. This is the case behind the # bbappend: `unittest' is reached only from there, and there is no there. check $P/no-tests-dir test ! -d "$PKGDIR/tests" check_eq $P/no-tests-anywhere "" \ "$(find "$PKGDIR" -type d -name tests 2>/dev/null)" check $P/no-cli test ! -e /usr/bin/oauthlib # --------------------------------------------------------------- dpkg file list --- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # Pure Python: the package carries no compiled extension, asserted directly # rather than trusted off the recipe's `inherit'. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" check_eq $P/tree-no-so "" \ "$(find "$PKGDIR" -name '*.so*' 2>/dev/null)" # Everything shipped is Python, its bytecode, or the dist-info. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" check_eq $P/no-dev-installed "" \ "$(sed -n '/^Package: python3-oauthlib-dev$/p' /var/lib/dpkg/status 2>/dev/null)" # ------------------------------------------------------------------- dist-info --- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 3.2.2" cat "$DI/METADATA" check_out $P/dist-info-name "Name: oauthlib" cat "$DI/METADATA" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" check_out $P/dist-info-top-level "oauthlib" cat "$DI/top_level.txt" check $P/dist-info-license test -s "$DI/LICENSE" check $P/dist-info-record test -s "$DI/RECORD" # ---------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-oauthlib$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" # The seven splits that ARE reached, each asserted by name. check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-crypt "python3-crypt" cat "$W/dep" check_out $P/pkg-depends-datetime "python3-datetime" cat "$W/dep" check_out $P/pkg-depends-json "python3-json" cat "$W/dep" check_out $P/pkg-depends-logging "python3-logging" cat "$W/dep" check_out $P/pkg-depends-math "python3-math" cat "$W/dep" check_out $P/pkg-depends-netclient "python3-netclient" cat "$W/dep" # THE ONE THIS PORT REMOVED. See the bbappend header: unittest is a test-only # dependency that reached the runtime package's Depends line. check_eq $P/pkg-depends-no-unittest "" \ "$(grep -o 'python3-unittest' "$W/dep" 2>/dev/null)" # ...and the split is still on the image on its own account, so the removal # changed a declaration and not the installed set. A red HERE and a green above # would mean the removal went further than intended. check $P/import-unittest-still-works python3 -c 'import unittest' # NOT declared, and each reached only through a declared split's closure. # Asserted as ABSENT from the Depends line so a future edit that "helpfully" # adds them is visible as a change of policy rather than arriving unnoticed. check_eq $P/pkg-depends-no-stringold "" \ "$(grep -o 'python3-stringold' "$W/dep" 2>/dev/null)" check_eq $P/pkg-depends-no-email "" \ "$(grep -o 'python3-email' "$W/dep" 2>/dev/null)" # The optional extras are OFF, so none of their packages is installed. If one of # these ever arrives, the RSA cases below stop meaning what they say. check_eq $P/no-pyjwt-installed "" \ "$(sed -n '/^Package: python3-pyjwt$/p' /var/lib/dpkg/status 2>/dev/null)" check_eq $P/no-cryptography-installed "" \ "$(sed -n '/^Package: python3-cryptography$/p' /var/lib/dpkg/status 2>/dev/null)" check_eq $P/no-blinker-installed "" \ "$(sed -n '/^Package: python3-blinker$/p' /var/lib/dpkg/status 2>/dev/null)" # --------------------------------------------------------------------- imports --- check $P/import python3 -c 'import oauthlib' check $P/import-oauth1 python3 -c 'import oauthlib.oauth1' check $P/import-oauth2 python3 -c 'import oauthlib.oauth2' check $P/import-openid python3 -c 'import oauthlib.openid' check $P/import-signature python3 -c 'from oauthlib.oauth1.rfc5849 import signature' check $P/import-clients python3 -c 'from oauthlib.oauth2 import WebApplicationClient, BackendApplicationClient' check_eq $P/import-quiet "" \ "$(python3 -c 'import oauthlib' 2>&1)" # ----------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present bst_probe $P/probe-run "$W/probe" 1500 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "89" "$(pv emitted_count)" # ------------------------------------------------------------------- identity --- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "3.2.2" "$(pv version_attr)" # __version__ is a LITERAL in oauthlib/__init__.py while the dist-info version # comes from the installed metadata; asserting both and their agreement is what # catches a package whose code and metadata were staged from different versions. check_eq $P/version-metadata "3.2.2" "$(pv version_metadata)" check_eq $P/version-agrees "yes" "$(pv version_agrees)" # ================= OAuth 1.0a -- RFC 5849 section 3.4.1 worked example ========= # # Three stages, each scored separately: a single end-to-end case would go red for # any of collection, normalisation or the digest and the console would not say # which. # --- stage 1: parameter collection (3.4.1.3.1) -------------------------------- # The RFC's table has ELEVEN rows and excludes oauth_signature and realm -- the # half implementations get wrong, because both are present in the Authorization # header the parameters are collected FROM. check_eq $P/collect-count "11" "$(pv collect_count)" check_eq $P/collect-excludes-signature "no" "$(pv collect_has_signature)" check_eq $P/collect-excludes-realm "no" "$(pv collect_has_realm)" check_eq $P/collect-pairs \ "a2=r b,a3=2 q,a3=a,b5==%3D,c2=,c@=,oauth_consumer_key=9djdj82h48djs9d2,oauth_nonce=7d8f3e4a,oauth_signature_method=HMAC-SHA1,oauth_timestamp=137131201,oauth_token=kkk9d7dh3k39sjv7" \ "$(pv collect_pairs)" # --- stage 2: parameter normalisation (3.4.1.3.2) ----------------------------- check_eq $P/normalized \ "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" \ "$(pv normalized)" # The same value, compared against the RFC's text inside the probe. This case and # the one above fail together if oauthlib is wrong and separately if the suite's # literal is stale, which is why both are kept. check_eq $P/normalized-matches-rfc "yes" "$(pv normalized_matches_rfc)" # --- stage 3: the signature base string (3.4.1.1) ----------------------------- check_eq $P/base-string-uri "http://example.com/request" "$(pv base_string_uri)" check_eq $P/signature-base-string \ "POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk9d7dh3k39sjv7" \ "$(pv signature_base_string)" check_eq $P/base-matches-rfc "yes" "$(pv base_matches_rfc)" # The method is upper-cased whatever case it arrives in (3.4.1.1 step 1). A # lowercase method that produced a different base string would verify against # itself and fail against every server. check_eq $P/base-method-case-insensitive "yes" "$(pv base_method_lowercase_same)" # --- base-string URI normalisation (3.4.1.2) ---------------------------------- # Each is a case the RFC calls out and a naive urlsplit()+format gets wrong. check_eq $P/bsu-default-port-dropped "http://example.com/request" "$(pv bsu_default_port_dropped)" check_eq $P/bsu-https-default-dropped "https://example.com/request" "$(pv bsu_https_default_dropped)" check_eq $P/bsu-nondefault-port-kept "https://www.example.net:8080/" "$(pv bsu_nondefault_port_kept)" check_eq $P/bsu-scheme-lowercased "http://example.com/request" "$(pv bsu_scheme_lowercased)" check_eq $P/bsu-empty-path-slash "http://www.example.com/" "$(pv bsu_empty_path_slash)" # The query is DISCARDED here: it is carried by the normalised parameters, and # including it in both would sign it twice. check_eq $P/bsu-query-dropped "https://www.example.net:8080/" "$(pv bsu_query_dropped)" # RFC 5849 3.4.1.2 example 1: an already-encoded path is preserved as-is. check_eq $P/bsu-pct-path-preserved "http://example.com/r%20v/X" "$(pv bsu_pct_path_preserved)" # --- percent-encoding (3.6) --------------------------------------------------- # THE CASES THAT TRIP A NAIVE QUOTER, each a real interoperability fault: # "~" is UNRESERVED in RFC 3986 and must stay literal. Python's own # urllib.parse.quote left it ENCODED until 3.7, so a port reaching for # quote() with the defaults encodes it and every signature carrying a # tilde differs from the server's. # " " must become %20 and never "+" -- "+" is the FORM-encoding rule, and # mixing the two is the classic OAuth 1 fault. (Note the contrast with # the OAuth 2 cases further down, where "+" is correct.) # "/" and ":" are RESERVED here, unlike in a path, so quote()'s default # safe="/" is wrong. check_eq $P/escape-tilde "~" "$(pv escape_tilde)" check_eq $P/escape-space "%20" "$(pv escape_space)" check_eq $P/escape-plus "%2B" "$(pv escape_plus)" check_eq $P/escape-slash "%2F" "$(pv escape_slash)" check_eq $P/escape-colon "%3A" "$(pv escape_colon)" check_eq $P/escape-unreserved "aZ0-._~" "$(pv escape_unreserved)" check_eq $P/escape-equals-amp "%3D%26" "$(pv escape_equals_amp)" # Hex digits are UPPERCASE per 3.6 -- "%2F", not "%2f". check_eq $P/escape-hex-uppercase "%2F" "$(pv escape_hex_uppercase)" check_eq $P/escape-nonascii "%C3%A5" "$(pv escape_nonascii)" check_eq $P/escape-percent "%25" "$(pv escape_percent)" # escape() takes text, not bytes; the class it raises is the contract. check_eq $P/escape-bytes-raises "!ValueError" "$(pv escape_bytes_raises)" # --- HMAC-SHA1 (3.4.2) -------------------------------------------------------- # RFC 5849 publishes an example oauth_signature but NOT the secrets behind it, # so that value cannot be recomputed by anyone. These use the probe's own # secrets, and the digest is cross-checked against hashlib/hmac directly. check_eq $P/hmac-sha1 "7hk0r+8N1nuFXSEOjynWjlzQcrg=" "$(pv hmac_sha1)" check_eq $P/hmac-sha1-matches-stdlib "yes" "$(pv hmac_sha1_matches_stdlib)" # The stdlib-computed digest itself, pinned. `matches-stdlib' above proves the # two agree; this proves WHICH value they agree on, so an arm where both were # wrong in the same way is still caught. check_eq $P/hmac-sha1-stdlib "7hk0r+8N1nuFXSEOjynWjlzQcrg=" "$(pv hmac_sha1_stdlib)" # An EMPTY token secret still contributes its "&" separator (3.4.2). A port that # dropped it would sign request-token calls wrongly and nothing else -- a fault # that hides until the first leg of the flow. check_eq $P/hmac-sha1-empty-token "Cz2XkNrhhu/mc60P6A1OxO2z6IM=" "$(pv hmac_sha1_empty_token)" check_eq $P/hmac-sha1-empty-token-matches "yes" "$(pv hmac_sha1_empty_token_matches)" check_eq $P/hmac-sha1-token-changes-sig "yes" "$(pv hmac_sha1_differs_without_token)" check_eq $P/hmac-sha256 "JFICiY6BMKSVnWnVjC+knpbJi0G50OZ5yl4putz7xRA=" "$(pv hmac_sha256)" check_eq $P/hmac-sha256-differs-from-sha1 "yes" "$(pv hmac_sha256_differs)" # The API is ASYMMETRIC, and this is measured rather than assumed: sha1 and # sha256 have bare sign_hmac_* helpers, sha512 has ONLY a *_with_client form. # Writing sign_hmac_sha512() here would AttributeError -- which is how this was # found -- so the surface itself is the assertion. check_eq $P/has-sign-hmac-sha1 "yes" "$(pv has_sign_hmac_sha1)" check_eq $P/has-sign-hmac-sha256 "yes" "$(pv has_sign_hmac_sha256)" check_eq $P/no-bare-sign-hmac-sha512 "no" "$(pv has_sign_hmac_sha512)" check_eq $P/has-sign-hmac-sha512-with-client "yes" "$(pv has_sign_hmac_sha512_with_client)" # --- PLAINTEXT (3.4.4) -------------------------------------------------------- # In scope, and it is NOT a digest: the signature IS the escaped secret pair, so # it must not depend on the base string at all. check_eq $P/plaintext "j49sk3j29djd&dh893kd9sh9d" "$(pv plaintext)" check_eq $P/plaintext-is-key "yes" "$(pv plaintext_is_key)" check_eq $P/plaintext-ignores-base "yes" "$(pv plaintext_ignores_base)" check_eq $P/plaintext-empty-token "j49sk3j29djd&" "$(pv plaintext_empty_token)" # --- RSA-SHA1 is OUT OF SCOPE and its absence is the assertion ----------------- # The import is lazy and unguarded, so this raises at CALL time, not at import. # By CLASS: ModuleNotFoundError is an ImportError subclass, so a caller catching # ImportError still catches it. check_eq $P/rsa-sha1-raises "!ModuleNotFoundError" "$(pv rsa_sha1_raises)" check_eq $P/rsa-sha1-is-import-error "yes" "$(pv rsa_sha1_is_import_error)" check_eq $P/rsa-sha1-exc-class "ModuleNotFoundError" "$(pv rsa_sha1_exc_class)" check_eq $P/rsa-sha1-names-jwt "yes" "$(pv rsa_sha1_names_jwt)" check_eq $P/jwt-absent "yes" "$(pv jwt_absent)" check_eq $P/cryptography-absent "yes" "$(pv cryptography_absent)" check_eq $P/blinker-absent "yes" "$(pv blinker_absent)" # signals.py degrades to a no-op without blinker; that IS the shipped behaviour # with PACKAGECONFIG[signals] off, and a "yes" here would mean an extra crept in. check_eq $P/signals-unavailable "no" "$(pv signals_available)" # ========================= OAuth 2.0 -- RFC 6749 ============================== # # --- authorisation-request URI construction (4.1.1) --------------------------- check_eq $P/grant-uri-basic \ "https://server.example.com/authorize?response_type=code&client_id=s6BhdRkqt3" \ "$(pv grant_uri_basic)" check_eq $P/grant-uri-full \ "https://server.example.com/authorize?response_type=code&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&scope=read+write&state=xyz" \ "$(pv grant_uri_full)" # NOTE THE "+" -- and it is correct here, unlike in OAuth 1. This is # application/x-www-form-urlencoded query construction (urlencode), where space # is "+"; RFC 5849's percent-encoding, where space is %20, is a different rule # for a different string. The two live in one package and the pair of cases is # what stops a well-meaning "fix" to either. check_eq $P/grant-uri-scope-list "https://e/a?response_type=code&client_id=cid&scope=a+b" "$(pv grant_uri_scope_list)" # A list and a space-joined string are the same scope, and must build the same URI. check_eq $P/grant-uri-scope-str "https://e/a?response_type=code&client_id=cid&scope=a+b" "$(pv grant_uri_scope_str)" # An existing query on the endpoint is PRESERVED and appended to, not replaced. check_eq $P/grant-uri-keeps-query "https://e/a?x=1&response_type=code&client_id=cid" "$(pv grant_uri_keeps_query)" # --- token-response parsing (5.1), from committed bodies ---------------------- check_eq $P/token-access "2YotnFZFEjr1zCsicMWpAA" "$(pv token_access)" check_eq $P/token-type "Bearer" "$(pv token_type)" check_eq $P/token-refresh "tGzv3JOkF0XG5Qx2TlKWIA" "$(pv token_refresh)" check_eq $P/token-expires-in "3600" "$(pv token_expires_in)" # expires_at is derived from expires_in and the CLOCK, so its value is not # assertable -- its presence is. check_eq $P/token-has-expires-at "yes" "$(pv token_has_expires_at)" check_eq $P/token-scope-list "read,write" "$(pv token_scope_list)" # A server that NARROWS the scope is a change the client must not miss, so # oauthlib raises rather than letting it believe it got what it asked for. check_eq $P/token-scope-changed-raises "!Warning" "$(pv token_scope_changed_raises)" # --- the named error responses (5.2) ------------------------------------------ # Each asserted by the CLASS it raises: a server returning one of these and a # client that merely saw "some error" is what these names exist to prevent. check_eq $P/err-invalid-grant "!InvalidGrantError" "$(pv err_invalid_grant)" check_eq $P/err-invalid-scope "!InvalidScopeError" "$(pv err_invalid_scope)" check_eq $P/err-invalid-client "!InvalidClientError" "$(pv err_invalid_client)" check_eq $P/err-unauthorized-client "!UnauthorizedClientError" "$(pv err_unauthorized_client)" check_eq $P/err-unsupported-grant-type "!UnsupportedGrantTypeError" "$(pv err_unsupported_grant_type)" check_eq $P/err-access-denied "!AccessDeniedError" "$(pv err_access_denied)" # A body with neither access_token nor error is not a token response either. check_eq $P/err-missing-token "!MissingTokenError" "$(pv err_missing_token)" # `invalid_request' RESOLVES TO InvalidClientIdError, WHICH IS NOT A TYPO. # # errors.raise_from_error() walks inspect.getmembers(module, isclass) -- which # returns members sorted ALPHABETICALLY -- and raises the first class whose # `.error' equals the slug. Several classes carry error = 'invalid_request': # InvalidRequestFatalError declares it and InvalidClientIdError, # InvalidRedirectURIError, MismatchingRedirectURIError and MissingClientIdError # all INHERIT it, as does InvalidRequestError separately. "InvalidClientIdError" # sorts first, so it wins. # # This is an upstream defect -- the slug dispatch is ambiguous and alphabetical # order decides it -- and it matters to a caller: the class raised is a # FatalClientError subclass, so `except InvalidRequestError' does NOT catch it. # It is oauthlib's behaviour and not this port's, it is identical on the x86-64 # control, and per `no-public-upstream-reports' nothing is filed against the # upstream project. It is pinned HERE so that a future oauthlib that fixes it is # visible as a change rather than passing unnoticed. check_eq $P/err-invalid-request-resolves-to-clientid "!InvalidClientIdError" "$(pv err_invalid_request)" # --- the state round-trip (4.1.2), which is the CSRF defence ------------------ check_eq $P/code-response-code "SplxlOBeZQQYbYS6WxSbIA" "$(pv code_response_code)" check_eq $P/code-response-state "xyz" "$(pv code_response_state)" # A mismatched state MUST raise rather than return a code -- this is the whole # of the flow's CSRF protection. check_eq $P/state-mismatch-raises "!MismatchingStateError" "$(pv state_mismatch_raises)" # ...and a MISSING state where one was expected is the same failure, not a pass. check_eq $P/state-missing-raises "!MismatchingStateError" "$(pv state_missing_raises)" # An error returned on the redirect is raised by class too. check_eq $P/code-response-error "!AccessDeniedError" "$(pv code_response_error)" check_eq $P/errors-are-oauth2error "yes" "$(pv errors_are_oauth2error)" # http:// is refused for OAuth 2 (1.6) -- a security property worth pinning. check_eq $P/insecure-transport-raises "!InsecureTransportError" "$(pv insecure_transport_raises)" # ------------------------------------------------------------------- hygiene --- # NOTHING OPENED A SOCKET. Measured, not asserted: the probe replaces # socket.socket, socket.create_connection and socket.getaddrinfo with raising # stand-ins BEFORE importing oauthlib, and every case above runs under them. # # Asserting `socket' is absent from sys.modules would be WRONG and would red a # correct package -- oauth1/rfc5849/utils.py imports urllib.request, which # imports socket at module scope. The module being present is not the question; # a connection being made is. check_eq $P/no-socket-opened "yes" "$(pv no_socket_opened)" check_eq $P/no-socket-calls "" "$(pv socket_calls)" check_eq $P/common-urlencoded-is-set "yes" "$(pv common_urlencoded_is_set)" rm -rf "$W"