From a65df33ec0d8217ddfe385b4327716b8b7c490c3 Mon Sep 17 00:00:00 2001 From: MagentaManifold <17zhaomingyuan@gmail.com> Date: Tue, 28 Apr 2026 13:40:36 -0400 Subject: [PATCH 1/3] feat: send WAF bypass header when env var is set Because: * we need a way to send WAF header to bypass rate limiting in CI environments. This commit: * sends fxa-ci=CI_WAF_TOKEN in header Closes FXA-13608 --- README.rst | 13 +++++++++++++ fxa/_utils.py | 5 +++++ fxa/tests/test_core.py | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/README.rst b/README.rst index de2df03..d52c516 100644 --- a/README.rst +++ b/README.rst @@ -89,6 +89,19 @@ testing with live email addresses. It works like this: client.destroy_account(acct.email, "MySecretPassword") +CI WAF bypass +============= + +When running CI tests against a Mozilla Accounts environment protected by +WAF, requests may be subject to challenge rules. +To bypass them, set the ``CI_WAF_TOKEN`` environment variable:: + + export CI_WAF_TOKEN= + +When this variable is set, PyFxA automatically includes the ``fxa-ci: `` +header on every outgoing request. When it is absent, no such header is sent. + + Passing tokens to other applications =================================================== diff --git a/fxa/_utils.py b/fxa/_utils.py index e333ce3..bb70d37 100644 --- a/fxa/_utils.py +++ b/fxa/_utils.py @@ -147,6 +147,7 @@ class APIClient: * backoff protocol support * sensible request timeouts * timestamp skew tracking with automatic retry on clockskew error + * CI WAF bypass header injection """ @@ -160,6 +161,10 @@ def __init__(self, server_url, session=None): allowed_methods={"DELETE", "GET", "POST", "PUT"}, ) session.mount(server_url, HTTPAdapter(max_retries=retries)) + # Inject CI WAF bypass header + waf_token = os.environ.get("CI_WAF_TOKEN") + if waf_token: + session.headers["fxa-ci"] = waf_token # Properties that can be customized to change behaviour. self.server_url = server_url self.timeout = 30 diff --git a/fxa/tests/test_core.py b/fxa/tests/test_core.py index f2cc92f..80e5772 100644 --- a/fxa/tests/test_core.py +++ b/fxa/tests/test_core.py @@ -7,10 +7,12 @@ import pyotp import pytest +import requests from parameterized import parameterized_class import fxa.errors from fxa.core import Client, StretchedPassword +from fxa._utils import APIClient from fxa.tests.utils import ( unittest, @@ -395,6 +397,28 @@ def test_totp(self): self.assertFalse(self.session.totp_exists()) +class TestAPIClientWAFHeader(unittest.TestCase): + """Unit tests for CI_WAF_TOKEN header injection in APIClient.""" + + SERVER_URL = "https://api.example.com/v1/" + + def test_waf_header_set_when_env_var_present(self): + with unittest.mock.patch.dict("os.environ", {"CI_WAF_TOKEN": "sekrit"}): + client = APIClient(self.SERVER_URL) + self.assertEqual(client.headers.get("fxa-ci"), "sekrit") + + def test_waf_header_absent_when_env_var_not_set(self): + with unittest.mock.patch.dict("os.environ", {}, clear=True): + client = APIClient(self.SERVER_URL) + self.assertNotIn("fxa-ci", client.headers) + + def test_waf_header_not_injected_into_caller_supplied_session(self): + supplied = requests.Session() + with unittest.mock.patch.dict("os.environ", {"CI_WAF_TOKEN": "sekrit"}): + APIClient(self.SERVER_URL, session=supplied) + self.assertNotIn("fxa-ci", supplied.headers) + + # helpers def verify_account(acct, client): def wait_for_email(m): From 6b6746e8847ab4d9e8c8c140320e7a76b57d331e Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Thu, 30 Apr 2026 16:41:57 -0700 Subject: [PATCH 2/3] fix: inject WAF bypass header regardless of session source --- README.rst | 2 +- fxa/_utils.py | 7 +++---- fxa/tests/test_core.py | 8 +++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index d52c516..d6640b5 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,7 @@ testing with live email addresses. It works like this: CI WAF bypass ============= -When running CI tests against a Mozilla Accounts environment protected by +When running CI tests against a Mozilla Accounts environment protected by WAF, requests may be subject to challenge rules. To bypass them, set the ``CI_WAF_TOKEN`` environment variable:: diff --git a/fxa/_utils.py b/fxa/_utils.py index bb70d37..7daeace 100644 --- a/fxa/_utils.py +++ b/fxa/_utils.py @@ -161,10 +161,9 @@ def __init__(self, server_url, session=None): allowed_methods={"DELETE", "GET", "POST", "PUT"}, ) session.mount(server_url, HTTPAdapter(max_retries=retries)) - # Inject CI WAF bypass header - waf_token = os.environ.get("CI_WAF_TOKEN") - if waf_token: - session.headers["fxa-ci"] = waf_token + waf_token = os.environ.get("CI_WAF_TOKEN") + if waf_token: + session.headers["fxa-ci"] = waf_token # Properties that can be customized to change behaviour. self.server_url = server_url self.timeout = 30 diff --git a/fxa/tests/test_core.py b/fxa/tests/test_core.py index 80e5772..85df34b 100644 --- a/fxa/tests/test_core.py +++ b/fxa/tests/test_core.py @@ -1,6 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +import os import time from urllib.parse import urlparse @@ -408,15 +409,16 @@ def test_waf_header_set_when_env_var_present(self): self.assertEqual(client.headers.get("fxa-ci"), "sekrit") def test_waf_header_absent_when_env_var_not_set(self): - with unittest.mock.patch.dict("os.environ", {}, clear=True): + env = {k: v for k, v in os.environ.items() if k != "CI_WAF_TOKEN"} + with unittest.mock.patch.dict("os.environ", env, clear=True): client = APIClient(self.SERVER_URL) self.assertNotIn("fxa-ci", client.headers) - def test_waf_header_not_injected_into_caller_supplied_session(self): + def test_waf_header_set_on_caller_supplied_session(self): supplied = requests.Session() with unittest.mock.patch.dict("os.environ", {"CI_WAF_TOKEN": "sekrit"}): APIClient(self.SERVER_URL, session=supplied) - self.assertNotIn("fxa-ci", supplied.headers) + self.assertEqual(supplied.headers.get("fxa-ci"), "sekrit") # helpers From bf2b4de1b38b46baafab40d9d66aa14208a0a487 Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Thu, 30 Apr 2026 17:12:31 -0700 Subject: [PATCH 3/3] fix: skip broken live tests by default, opt-in with env var --- fxa/tests/test_core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fxa/tests/test_core.py b/fxa/tests/test_core.py index 85df34b..979b98c 100644 --- a/fxa/tests/test_core.py +++ b/fxa/tests/test_core.py @@ -38,6 +38,8 @@ class TestCoreClient(unittest.TestCase): server_url = TEST_SERVER_URL def setUp(self): + if not os.environ.get("FXA_RUN_LIVE_TESTS"): + self.skipTest("Set FXA_RUN_LIVE_TESTS=1 to run live tests against the stage server") self.client_v1 = Client(self.server_url) self.client_v2 = Client(self.server_url, key_stretch_version=2) if self.key_stretch_version == 2: @@ -285,7 +287,8 @@ class TestCoreClientSession(unittest.TestCase): server_url = TEST_SERVER_URL def setUp(self): - + if not os.environ.get("FXA_RUN_LIVE_TESTS"): + self.skipTest("Set FXA_RUN_LIVE_TESTS=1 to run live tests against the stage server") self.client_v2 = Client(self.server_url, key_stretch_version=2) self.client_v1 = Client(self.server_url, key_stretch_version=1) if self.key_stretch_version == 2: