. "$BST_LIB" # BST-TIMEOUT: 2400 # SIX python3 starts, and they are the whole cost. Every # # other assertion reads the filesystem or dpkg with shell # # builtins, or greps the output of one of those six. # # # # Five are one-line imports -- the package, one submodule, # # the four exported names, a bare import whose STDERR is the # # assertion (`import-quiet'), and the two dependencies -- # # and the sixth is the probe, which takes ONE start for 95 # # measurements. # # # # The work inside the probe is one HMAC-SHA1 over 200 bytes, # # a dozen dict round-trips and about twenty PreparedRequest # # builds. There is NO network and no sleep in it at all: the # # sessions are mounted on a transport adapter that returns a # # committed body, so the probe's wall-clock is interpreter # # start plus microseconds. # # # # 2400 is 400s per start, the same per-start budget # # 99-python3-flexcache and 99-python3-dateutil set. # # # # 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-requests-oauthlib 2.0.0 (meta-openembedded/meta-python # recipes-devtools/python/python3-requests-oauthlib_2.0.0.bb, `inherit pypi # setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE IS, AND WHY THE CASES LOOK AS THEY DO. # ============================================================================ # # It is an ADAPTER and nothing else. oauthlib owns the crypto and the protocol # (BRINTOS-2406), requests owns the transport (BRINTOS-2240), and this package # is four objects that join them: OAuth1/OAuth2, which are requests AuthBase # handlers, and OAuth1Session/OAuth2Session, which are requests.Session # subclasses carrying one of those handlers plus the flow helpers. # # So this suite measures THE SEAM -- which header lands on the request, which # exception reaches the caller, what the token dict looks like after a fetch -- # and not HMAC-SHA1, which is oauthlib's. The two RFC 5849 anchors below are the # exception, and they are there to separate a broken hash from a broken adapter. # # ============================================================================ # NO NETWORK AND NO SERVER, BY CONSTRUCTION RATHER THAN BY CONVENTION. # ============================================================================ # # Every session in the probe has a `TapAdapter' mounted on https:// and http://. # requests reaches a socket through exactly one method -- the mounted adapter's # send() -- so replacing it replaces the whole transport: the PreparedRequest is # recorded and a committed body is returned. Nothing resolves a name, nothing # opens a socket, and the run is deterministic and bounded on any arm. # # That is also what makes the strongest cases here possible at all. `A SESSION # WITH NO CREDENTIALS REFUSES TO SIGN' is asserted as the exception class AND as # the adapter's request count being zero -- an implementation that fell through # and sent the request unsigned would raise nothing, and only the count catches # it. The same shape covers the state-mismatch path, where a token exchange # performed before the check would leak the authorization code. # # ============================================================================ # WHY THERE IS NO BBAPPEND. # ============================================================================ # # meta-python's recipe is taken unmodified: no patch, no site answer, no # override, and no RDEPENDS correction. The package is pure Python -- two auth # modules, two session modules and ten single-function compliance fixes -- so # there is no configure step, no compile step and no C for an override to reach, # and meta-python's `RDEPENDS:${PN} += "python3-requests python3-oauthlib"' is # both necessary and (measured below, by refusal on an x86-64 control) # sufficient for the module-scope imports. # # This image's python3 is ONE STATIC BINARY with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so a module carrying a compiled extension could # not be imported whatever it built to. `pkg-no-so' and `tree-no-so' assert that # negative directly rather than trusting the recipe's `inherit'. # # ============================================================================ # WHERE THE EXPECTATIONS COME FROM. # ============================================================================ # # Every literal below was measured on an x86-64 control at CPython 3.12.3 # running THIS probe against requests-oauthlib 2.0.0 over oauthlib 3.2.2 and # requests 2.32.4 -- the same three versions this image ships, requests pinned # down from the control's default 2.34.2 on purpose so the control is not # answering a different question from the target. # # TWO EXCEPTIONS, and they are the anchors: `rfc5849-base-string' and # `rfc5849-signature' are compared against RFC 5849 section 3.4.1.1's own # printed bytes, not against a control run. # # ============================================================================ # ONE EXPECTATION THAT LOOKS WRONG AND IS NOT. # ============================================================================ # # `oauth1-header' does NOT carry RFC 5849 section 1.2's printed signature # (74KNZJeDHnMBp0EMJ9ZHt/XKycU=) even though it is built from that example's # consumer key, secret, callback, nonce and timestamp. oauthlib appends # `oauth_version="1.0"' unconditionally (Client.get_oauth_params), the RFC's # example omits it, and the version parameter is IN the signature base string -- # so the two signatures cannot agree and a suite asserting the RFC's string here # would red on a correct library. What is asserted is the header oauthlib # produces for those pinned inputs, plus `oauth1-repeatable' and # `oauth1-nonce-changes-signature', which are what make a pinned expectation # legitimate rather than a coincidence. # # ============================================================================ # THIS PORT LANDS AFTER python3-oauthlib (BRINTOS-2406). # ============================================================================ # # meta-python declares `RDEPENDS:${PN} += "python3-requests python3-oauthlib"'. # python3-requests is on this image already (BRINTOS-2240); python3-oauthlib is # a sibling of this same round, so until it is on the image the install of THIS # package fails and every case below is unreachable. `pkg-depends-oauthlib' # asserts the declaration, and `import-oauthlib' asserts the arrival -- a red on # the second with the first green says the sibling has not landed yet, which is # a scheduling fact and not a defect in this port. P=python3-requests-oauthlib bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/requests_oauthlib CFDIR=$PKGDIR/compliance_fixes DI=$SITE/requests_oauthlib-2.0.0.dist-info PKGLIST=/var/lib/dpkg/info/python3-requests-oauthlib.list PROBE=$BST_DATA/python3-requests-oauthlib-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-reqoauth.$$ 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/requests_oauthlib.py" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/oauth1-auth-py test -s "$PKGDIR/oauth1_auth.py" check $P/oauth1-session-py test -s "$PKGDIR/oauth1_session.py" check $P/oauth2-auth-py test -s "$PKGDIR/oauth2_auth.py" check $P/oauth2-session-py test -s "$PKGDIR/oauth2_session.py" check_eq $P/module-count "5" \ "$(find "$PKGDIR" -maxdepth 1 -name '*.py' 2>/dev/null | wc -l | tr -d ' ')" check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" check $P/pycache-oauth1 test -s "$PKGDIR/__pycache__/oauth1_session.cpython-312.pyc" check $P/pycache-oauth2 test -s "$PKGDIR/__pycache__/oauth2_session.cpython-312.pyc" # The compliance fixes are nine per-provider workarounds plus the package marker, # for servers that do not answer to spec -- the roster of the 2.0.0 sdist. It is # asserted by name, not only by count, so a future upstream add or drop names the # file that moved instead of printing an off-by-one. check $P/compliance-dir test -d "$CFDIR" check $P/compliance-init test -f "$CFDIR/__init__.py" check_eq $P/compliance-module-count "10" \ "$(find "$CFDIR" -maxdepth 1 -name '*.py' 2>/dev/null | wc -l | tr -d ' ')" check_eq $P/compliance-roster \ "__init__.py,douban.py,ebay.py,facebook.py,fitbit.py,instagram.py,mailchimp.py,plentymarkets.py,slack.py,weibo.py" \ "$(find "$CFDIR" -maxdepth 1 -name '*.py' 2>/dev/null | sed 's#.*/##' | LC_ALL=C sort | paste -sd, - | tr -d ' ')" check $P/compliance-facebook test -s "$CFDIR/facebook.py" check $P/compliance-slack test -s "$CFDIR/slack.py" # A library, not a program: nothing lands in $PATH and the dist-info declares no # console script. A red here means the recipe grew an entry point nobody asked # for, which on this image is a file that could never be executed anyway. check $P/no-cli test ! -e /usr/bin/requests-oauthlib check $P/no-entry-points test ! -f "$DI/entry_points.txt" # --------------------------------------------------------------- dpkg file list --- cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" 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 the package ships is Python, its bytecode, or the dist-info. There # is no py.typed and no data file, so unlike the flexcache suite next door this # filter needs no per-file exemption -- and a new one appearing is exactly what # the case is for. _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-requests-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: 2.0.0" cat "$DI/METADATA" check_out $P/dist-info-name "Name: requests-oauthlib" cat "$DI/METADATA" check_out $P/dist-info-top-level "requests_oauthlib" cat "$DI/top_level.txt" check $P/dist-info-record test -s "$DI/RECORD" # ---------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-requests-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" check_out $P/pkg-version "Version: 2.0.0-r0" cat "$W/st" grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" # The two meta-python declares by hand, and the stdlib splits pypi/setuptools3 # resolves. python3-oauthlib is the SIBLING this port lands after -- see the # header; the declaration is asserted here and the arrival at `import-oauthlib'. check_out $P/pkg-depends-requests "python3-requests" cat "$W/dep" check_out $P/pkg-depends-oauthlib "python3-oauthlib" cat "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" # --------------------------------------------------------------------- imports --- check $P/import python3 -c 'import requests_oauthlib' check $P/import-submodule python3 -c 'import requests_oauthlib.oauth2_session' check $P/import-names python3 -c 'from requests_oauthlib import OAuth1, OAuth1Session, OAuth2, OAuth2Session' check_eq $P/import-quiet "" \ "$(python3 -c 'import requests_oauthlib' 2>&1)" # The two runtime dependencies, imported on their own so a red separates "the # sibling has not landed" and "requests is broken" from "this adapter is broken". check $P/import-oauthlib python3 -c 'import oauthlib' check $P/import-requests python3 -c 'import requests' # ----------------------------------------------------------------------- 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" 1800 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 "95" "$(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 "2.0.0" "$(pv version_attr)" check_eq $P/version-metadata "2.0.0" "$(pv version_metadata)" # The two the seam is made of. Their versions are asserted because every literal # below was measured against exactly these, so a bump underneath this suite # should say so here rather than as a scatter of unexplained reds. check_eq $P/oauthlib-version "3.2.2" "$(pv oauthlib_version)" check_eq $P/requests-version "2.32.4" "$(pv requests_version)" check_eq $P/has-dunder-all "no" "$(pv has_dunder_all)" check_eq $P/exports-present "OAuth1,OAuth1Session,OAuth2,OAuth2Session" \ "$(pv exports_present)" check_eq $P/oauth1-is-authbase "yes" "$(pv oauth1_is_authbase)" check_eq $P/oauth2-is-authbase "yes" "$(pv oauth2_is_authbase)" check_eq $P/oauth1session-is-session "yes" "$(pv oauth1session_is_session)" check_eq $P/oauth2session-is-session "yes" "$(pv oauth2session_is_session)" check_eq $P/probe-no-so-in-tree "yes" "$(pv no_so_in_tree)" # ================================================================== OAuth 1.0a == # RFC 5849 section 1.2's temporary-credentials request with the nonce and the # timestamp pinned, so the header is a function of the inputs alone. check_eq $P/oauth1-requests-made "1" "$(pv oauth1_requests_made)" check_eq $P/oauth1-method "POST" "$(pv oauth1_method)" check_eq $P/oauth1-url "https://photos.example.net/initiate" "$(pv oauth1_url)" # THE HEADER, BYTE FOR BYTE -- oauthlib's parameter ORDER included, which is why # this is one case and the eight below are another: a red here alone says the # ordering moved, a red here and in one of the eight says a value did. check_eq $P/oauth1-header \ 'OAuth realm="Photos", oauth_nonce="wIjqoS", oauth_timestamp="137131200", oauth_version="1.0", oauth_signature_method="HMAC-SHA1", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_callback="http%3A%2F%2Fprinter.example.com%2Fready", oauth_signature="msrTmwtDEKqeVXeJaufuiXOpbJI%3D"' \ "$(pv oauth1_header)" check_eq $P/oauth1-param-names \ "oauth_callback,oauth_consumer_key,oauth_nonce,oauth_signature,oauth_signature_method,oauth_timestamp,oauth_version,realm" \ "$(pv oauth1_param_names)" check_eq $P/oauth1-consumer-key '"dpf43f3p2l4k3l03"' "$(pv oauth1_consumer_key)" check_eq $P/oauth1-nonce '"wIjqoS"' "$(pv oauth1_nonce)" check_eq $P/oauth1-timestamp '"137131200"' "$(pv oauth1_timestamp)" check_eq $P/oauth1-sigmethod '"HMAC-SHA1"' "$(pv oauth1_sigmethod)" check_eq $P/oauth1-version '"1.0"' "$(pv oauth1_version)" check_eq $P/oauth1-realm '"Photos"' "$(pv oauth1_realm)" check_eq $P/oauth1-callback '"http%3A%2F%2Fprinter.example.com%2Fready"' \ "$(pv oauth1_callback)" check_eq $P/oauth1-signature '"msrTmwtDEKqeVXeJaufuiXOpbJI%3D"' \ "$(pv oauth1_signature)" # The signature went into the Authorization header and NOWHERE else. A library # that had also put it in the query or the body would produce a different wire # request with an identical header, which no case above could see. check_eq $P/oauth1-body-empty "yes" "$(pv oauth1_body_empty)" check_eq $P/oauth1-query-empty "yes" "$(pv oauth1_query_empty)" # What makes the pinned expectation above legitimate: same inputs, same bytes; # one different nonce, different signature. check_eq $P/oauth1-repeatable "yes" "$(pv oauth1_repeatable)" check_eq $P/oauth1-nonce-changes-signature "yes" "$(pv oauth1_nonce_changes_signature)" # --- the RFC anchors ---------------------------------------------------------- # RFC 5849 section 3.4.1.1 prints its signature base string in full. This is the # one expectation in the file that comes from the RFC's own bytes rather than # from a control run, so a red here is a genuine protocol regression. check_eq $P/rfc5849-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 rfc5849_base_string)" check_eq $P/rfc5849-base-string-uri "http://example.com/request" \ "$(pv rfc5849_base_string_uri)" # ...and the RFC's own signature over it, with the RFC's two secrets. check_eq $P/rfc5849-signature "r6/TJjbCOr97/+UU0NsvSne7s5g=" "$(pv rfc5849_signature)" # Computed a second time straight out of hmac/hashlib. Equal answers separate a # broken hash on this arm from a broken adapter, which one number cannot. check_eq $P/rfc5849-sig-matches-stdlib "yes" "$(pv rfc5849_sig_matches_stdlib)" # --- no credentials, no request ----------------------------------------------- # The class AND the count. Falling through and sending the request unsigned is # the failure that would matter, and it raises nothing. check_eq $P/oauth1-unsigned-refused "!ValueError" "$(pv oauth1_unsigned_refused)" check_eq $P/oauth1-unsigned-requests-made "0" "$(pv oauth1_unsigned_requests_made)" # --- the three-legged flow, over committed bodies ------------------------------ check_eq $P/oauth1-request-token-keys \ "oauth_callback_confirmed,oauth_token,oauth_token_secret" \ "$(pv oauth1_request_token_keys)" check_eq $P/oauth1-request-token "hh5s93j4hdidpola" "$(pv oauth1_request_token)" check_eq $P/oauth1-request-token-secret "hdhd0244k9j7ao03" "$(pv oauth1_request_token_secret)" check_eq $P/oauth1-callback-confirmed "true" "$(pv oauth1_callback_confirmed)" check_eq $P/oauth1-token-is-dict "yes" "$(pv oauth1_token_is_dict)" # The fetch did not merely PARSE the body -- it armed the session with the token # it parsed, which is what the next leg depends on. check_eq $P/oauth1-session-token-set "yes" "$(pv oauth1_session_token_set)" check_eq $P/oauth1-authorization-url \ "https://photos.example.net/authorize?oauth_token=hh5s93j4hdidpola" \ "$(pv oauth1_authorization_url)" check_eq $P/oauth1-parsed-keys "oauth_token,oauth_verifier" "$(pv oauth1_parsed_keys)" check_eq $P/oauth1-parsed-verifier "hfdp7dh39dks9884" "$(pv oauth1_parsed_verifier)" # ==================================================================== OAuth 2 == check_eq $P/oauth2-authorization-url \ "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 oauth2_authorization_url)" check_eq $P/oauth2-state-returned "xyz" "$(pv oauth2_state_returned)" check_eq $P/oauth2-state-attr "xyz" "$(pv oauth2_state_attr)" # With no state given the session GENERATES one. The value is random, so what is # scored is the relationship -- present, carried into the URL, long enough to be # a CSRF token, and not the pinned one -- never the string. check_eq $P/oauth2-state-generated-nonempty "yes" "$(pv oauth2_state_generated_nonempty)" check_eq $P/oauth2-state-generated-in-url "yes" "$(pv oauth2_state_generated_in_url)" check_eq $P/oauth2-state-generated-len-ge-16 "yes" "$(pv oauth2_state_generated_len_ge_16)" check_eq $P/oauth2-state-generated-differs "yes" "$(pv oauth2_state_generated_differs)" check_eq $P/oauth2-authorized-before "no" "$(pv oauth2_authorized_before)" # --- token fetch, field by field ---------------------------------------------- check_eq $P/oauth2-token-keys \ "access_token,expires_at,expires_in,refresh_token,scope,token_type" \ "$(pv oauth2_token_keys)" check_eq $P/oauth2-access-token "2YotnFZFEjr1zCsicMWpAA" "$(pv oauth2_access_token)" check_eq $P/oauth2-token-type "Bearer" "$(pv oauth2_token_type)" check_eq $P/oauth2-expires-in "3600" "$(pv oauth2_expires_in)" check_eq $P/oauth2-refresh-token "tGzv3JOkF0XG5Qx2TlKWIA" "$(pv oauth2_refresh_token)" # The server sent `scope' as one space-separated string; oauthlib hands back a # LIST. That conversion is the seam doing its job and is asserted as such. check_eq $P/oauth2-scope-split "read,write" "$(pv oauth2_scope)" # expires_at is DERIVED (now + expires_in) and is therefore not a literal here; # what is asserted is that it was derived at all, and as a number. check_eq $P/oauth2-expires-at-is-number "yes" "$(pv oauth2_expires_at_is_number)" check_eq $P/oauth2-authorized-after "yes" "$(pv oauth2_authorized_after)" check_eq $P/oauth2-access-token-attr "2YotnFZFEjr1zCsicMWpAA" \ "$(pv oauth2_access_token_attr)" # --- what actually went out --------------------------------------------------- # The token request is where a client leaks its secret if the seam is wrong, so # the captured request is asserted rather than assumed. check_eq $P/oauth2-token-requests-made "1" "$(pv oauth2_token_requests_made)" check_eq $P/oauth2-token-method "POST" "$(pv oauth2_token_method)" check_eq $P/oauth2-token-url "https://server.example.com/token" "$(pv oauth2_token_url)" check_eq $P/oauth2-token-ctype "application/x-www-form-urlencoded" \ "$(pv oauth2_token_ctype)" check_eq $P/oauth2-token-body-params "code,grant_type,redirect_uri" \ "$(pv oauth2_token_body_params)" check_eq $P/oauth2-token-body-grant "yes" "$(pv oauth2_token_body_grant)" check_eq $P/oauth2-token-body-code "yes" "$(pv oauth2_token_body_code)" check_eq $P/oauth2-token-auth-scheme "Basic" "$(pv oauth2_token_auth_scheme)" check_eq $P/oauth2-token-basic-decodes "s6BhdRkqt3:7Fjfp0ZBr1KtDRbnfVdmIw" \ "$(pv oauth2_token_basic_decodes)" # The RFC prefers the Basic header over form fields, and this is the case that # says the secret did not ALSO go into the body -- the failure that leaves a # client secret in every proxy log the request passes. check_eq $P/oauth2-secret-not-in-body "yes" "$(pv oauth2_secret_not_in_body)" # The token the exchange bought, on the next request. check_eq $P/oauth2-bearer-header "Bearer 2YotnFZFEjr1zCsicMWpAA" \ "$(pv oauth2_bearer_header)" # --- refresh ------------------------------------------------------------------ check_eq $P/oauth2-refresh-keys \ "access_token,expires_at,expires_in,refresh_token,scope,token_type" \ "$(pv oauth2_refresh_keys)" check_eq $P/oauth2-refresh-access-token "NEWtnFZFEjr1zCsicMWpAA" \ "$(pv oauth2_refresh_access_token)" check_eq $P/oauth2-refresh-new-refresh-token "NEWv3JOkF0XG5Qx2TlKWIA" \ "$(pv oauth2_refresh_new_refresh_token)" check_eq $P/oauth2-refresh-replaced-session-token "yes" \ "$(pv oauth2_refresh_replaced_session_token)" check_eq $P/oauth2-refresh-body-grant "yes" "$(pv oauth2_refresh_body_grant)" check_eq $P/oauth2-refresh-requests-made "1" "$(pv oauth2_refresh_requests_made)" # --- the documented error paths ----------------------------------------------- # A state that does not match MUST raise BEFORE the exchange, or the client has # handed an attacker-supplied code to the token endpoint. The count is the half # of that assertion the exception class cannot make. check_eq $P/oauth2-state-mismatch "!MismatchingStateError" "$(pv oauth2_state_mismatch)" check_eq $P/oauth2-state-mismatch-requests-made "0" \ "$(pv oauth2_state_mismatch_requests_made)" # RFC 6749 section 5.2's error codes arrive as oauthlib's own classes rather # than as a 400 the caller has to parse. Two of them, because the mapping is # per-code and a single one would not show that. check_eq $P/oauth2-invalid-grant "!InvalidGrantError" "$(pv oauth2_invalid_grant)" check_eq $P/oauth2-invalid-client "!InvalidClientError" "$(pv oauth2_invalid_client)" # An expired token with no auto-refresh is the caller's to handle, and it is # refused before the request rather than sent and rejected. check_eq $P/oauth2-expired "!TokenExpiredError" "$(pv oauth2_expired)" check_eq $P/oauth2-expired-requests-made "0" "$(pv oauth2_expired_requests_made)" # ...and with auto-refresh armed, the SAME expired token drives a refresh and # then the resource request: two requests through the tap, the refresh first. check_eq $P/oauth2-autorefresh "" "$(pv oauth2_autorefresh)" check_eq $P/oauth2-autorefresh-requests-made "2" "$(pv oauth2_autorefresh_requests_made)" check_eq $P/oauth2-autorefresh-first-url "https://server.example.com/token" \ "$(pv oauth2_autorefresh_first_url)" check_eq $P/oauth2-autorefresh-token-replaced "yes" \ "$(pv oauth2_autorefresh_token_replaced)" # Arming auto-refresh without a token_updater is the documented TokenUpdated # exception, not a silent drop of the new token. check_eq $P/oauth2-no-updater "!TokenUpdated" "$(pv oauth2_no_updater)" check_eq $P/oauth2-compliance-hooks \ "access_token_request,access_token_response,protected_request,refresh_token_request,refresh_token_response" \ "$(pv oauth2_compliance_hooks)" # ------------------------------------------------------------------- hygiene --- # The probe worked under TMPDIR and left nothing behind. A cache or a token file # written into the rootfs would be a finding about the suite, not about the port. check_eq $P/probe-workdir-removed "yes" "$(pv workdir_removed)" check_eq $P/probe-cwd-unchanged "yes" "$(pv cwd_unchanged)" rm -rf "$W"