refactor: use configurable running service for Python integration tests (#2186)

* refactor: use configurable running service for Python integration tests

Instead of configuring and starting server instances within Python as
test fixtures, simply testing against a running server.  A mix of make
target and docker-compose changes is used to achieve the same level of
test coverage that previously relied on the (re-)configure and
(re-)start of the services in conftest.py.

A new `make` target, 'run_local_e2e_tests', can be used to run the
integration tests locally.  However, the stage FxA JWT validation tests
in test_e2e.py are excluded.  Those tests rely on the JWK configuration
of the Token Server and the stage FxA API, making them less "local".
Anyone working on that specific integration can certainly invoke those
tests themselves.

This patch also:
 - deletes some duplicate docs
 - moves the docker-compose yamls into a dir name 'docker'

* Apply suggestion from @pjenvey

Co-authored-by: Philip Jenvey <pjenvey@underboss.org>

* Apply suggestion from @pjenvey

Co-authored-by: Philip Jenvey <pjenvey@underboss.org>

* Clean-up based on feedback.

* Remove integration_tests/conftest.py

* Prune yamls

---------

Co-authored-by: Philip Jenvey <pjenvey@underboss.org>
This commit is contained in:
Barry Chen 2026-04-10 12:32:57 -05:00 committed by GitHub
parent 41e813e590
commit 9e89b6025e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 251 additions and 399 deletions

128
Makefile
View File

@ -69,61 +69,121 @@ clean:
cargo clean
docker_start_mysql:
docker compose -f docker-compose.mysql.yaml up -d
docker compose -f docker/docker-compose.mysql.yaml up -d
docker_start_mysql_rebuild:
docker compose -f docker-compose.mysql.yaml up --build -d
docker compose -f docker/docker-compose.mysql.yaml up --build -d
docker_stop_mysql:
docker compose -f docker-compose.mysql.yaml down
docker compose -f docker/docker-compose.mysql.yaml down
docker_start_spanner:
docker compose -f docker-compose.spanner.yaml up -d
docker compose -f docker/docker-compose.spanner.yaml up -d
docker_start_spanner_rebuild:
docker compose -f docker-compose.spanner.yaml up --build -d
docker compose -f docker/docker-compose.spanner.yaml up --build -d
docker_stop_spanner:
docker compose -f docker-compose.spanner.yaml down
docker compose -f docker/docker-compose.spanner.yaml down
.ONESHELL:
docker_run_mysql_e2e_tests:
exit_code=0
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E \
RESULTS_FILENAME=mysql_no_jwk_integration_results.xml \
docker compose \
-f docker-compose.mysql.yaml \
-f docker-compose.e2e.mysql.yaml \
-f docker/docker-compose.mysql.yaml \
-f docker/docker-compose.e2e.mysql.yaml \
-f docker/docker-compose.e2e.no-jwk-cache.yaml \
up \
--exit-code-from mysql-e2e-tests \
--abort-on-container-exit;
exit_code=$$?;
docker cp mysql-e2e-tests:/mysql_integration_results.xml ${MYSQL_INT_JUNIT_XML};
docker cp mysql-e2e-tests:/mysql_no_jwk_integration_results.xml ${MYSQL_NO_JWK_INT_JUNIT_XML};
exit $$exit_code;
--exit-code-from e2e-tests \
--abort-on-container-exit || exit_code=$$?
docker cp mysql-e2e-tests:/mysql_no_jwk_integration_results.xml ${MYSQL_NO_JWK_INT_JUNIT_XML}
RESULTS_FILENAME=mysql_integration_results.xml docker compose \
-f docker/docker-compose.mysql.yaml \
-f docker/docker-compose.e2e.mysql.yaml \
-f docker/docker-compose.e2e.jwk-cache.yaml \
up \
--exit-code-from e2e-tests \
--abort-on-container-exit || exit_code=$$?
docker cp mysql-e2e-tests:/mysql_integration_results.xml ${MYSQL_INT_JUNIT_XML}
docker compose \
-f docker/docker-compose.mysql.yaml \
-f docker/docker-compose.e2e.mysql.yaml \
down -v --remove-orphans
exit $$exit_code
.ONESHELL:
docker_run_postgres_e2e_tests:
exit_code=0
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E \
RESULTS_FILENAME=postgres_no_jwk_integration_results.xml \
docker compose \
-f docker-compose.postgres.yaml \
-f docker-compose.e2e.postgres.yaml \
-f docker/docker-compose.postgres.yaml \
-f docker/docker-compose.e2e.postgres.yaml \
-f docker/docker-compose.e2e.no-jwk-cache.yaml \
up \
--exit-code-from postgres-e2e-tests \
--abort-on-container-exit;
exit_code=$$?;
docker cp postgres-e2e-tests:/postgres_integration_results.xml ${POSTGRES_INT_JUNIT_XML};
docker cp postgres-e2e-tests:/postgres_no_jwk_integration_results.xml ${POSTGRES_NO_JWK_INT_JUNIT_XML};
exit $$exit_code;
--exit-code-from e2e-tests \
--abort-on-container-exit || exit_code=$$?
docker cp postgres-e2e-tests:/postgres_no_jwk_integration_results.xml ${POSTGRES_NO_JWK_INT_JUNIT_XML}
RESULTS_FILENAME=postgres_integration_results.xml docker compose \
-f docker/docker-compose.postgres.yaml \
-f docker/docker-compose.e2e.postgres.yaml \
-f docker/docker-compose.e2e.jwk-cache.yaml \
up \
--exit-code-from e2e-tests \
--abort-on-container-exit || exit_code=$$?
docker cp postgres-e2e-tests:/postgres_integration_results.xml ${POSTGRES_INT_JUNIT_XML}
docker compose \
-f docker/docker-compose.postgres.yaml \
-f docker/docker-compose.e2e.postgres.yaml \
down -v --remove-orphans
exit $$exit_code
.ONESHELL:
docker_run_spanner_e2e_tests:
exit_code=0
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N \
env -u SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E \
RESULTS_FILENAME=spanner_no_jwk_integration_results.xml \
docker compose \
-f docker-compose.spanner.yaml \
-f docker-compose.e2e.spanner.yaml \
-f docker/docker-compose.spanner.yaml \
-f docker/docker-compose.e2e.spanner.yaml \
-f docker/docker-compose.e2e.no-jwk-cache.yaml \
up \
--exit-code-from spanner-e2e-tests \
--abort-on-container-exit;
exit_code=$$?;
docker cp spanner-e2e-tests:/spanner_integration_results.xml ${SPANNER_INT_JUNIT_XML};
docker cp spanner-e2e-tests:/spanner_no_jwk_integration_results.xml ${SPANNER_NO_JWK_INT_JUNIT_XML};
exit $$exit_code;
--exit-code-from e2e-tests \
--abort-on-container-exit || exit_code=$$?
docker cp spanner-e2e-tests:/spanner_no_jwk_integration_results.xml ${SPANNER_NO_JWK_INT_JUNIT_XML}
RESULTS_FILENAME=spanner_integration_results.xml docker compose \
-f docker/docker-compose.spanner.yaml \
-f docker/docker-compose.e2e.spanner.yaml \
-f docker/docker-compose.e2e.jwk-cache.yaml \
up \
--exit-code-from e2e-tests \
--abort-on-container-exit || exit_code=$$?
docker cp spanner-e2e-tests:/spanner_integration_results.xml ${SPANNER_INT_JUNIT_XML}
docker compose \
-f docker/docker-compose.spanner.yaml \
-f docker/docker-compose.e2e.spanner.yaml \
down -v --remove-orphans
exit $$exit_code
run_mysql: $(INSTALL_STAMP)
# See https://github.com/PyO3/pyo3/issues/1741 for discussion re: why we need to set the
@ -180,6 +240,14 @@ run_token_server_integration_tests:
poetry install --no-root --without dev
poetry run pytest tools/tokenserver --junit-xml=${INTEGRATION_JUNIT_XML}
run_local_e2e_tests:
PYTHONPATH=$(PWD)/tools \
SYNC_MASTER_SECRET=$${SYNC_MASTER_SECRET:-secret0} \
SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL=$${SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL:-http://localhost:6000} \
TOKENSERVER_HOST=$${TOKENSERVER_HOST:-http://localhost:8000} \
poetry -C tools/integration_tests \
run pytest . --ignore=tokenserver/test_e2e.py
.PHONY: install
install: $(INSTALL_STAMP) ## Install dependencies with poetry
$(INSTALL_STAMP): pyproject.toml poetry.lock

View File

@ -1,47 +0,0 @@
services:
mysql-e2e-tests:
container_name: mysql-e2e-tests
depends_on:
sync-db:
condition: service_healthy
mock-fxa-server:
condition: service_started
tokenserver-db:
condition: service_healthy
# this depend is to avoid migration collisions.
# the syncserver isn't actually used for the tests,
# but collisions can happen particularly in CI.
syncserver:
condition: service_healthy
image: app:build
privileged: true
user: root
environment:
JWK_CACHE_DISABLED: false
MOCK_FXA_SERVER_URL: http://mock-fxa-server:6000
SYNC_HOST: 0.0.0.0
SYNC_MASTER_SECRET: secret0
SYNC_SYNCSTORAGE__DATABASE_URL: mysql://test:test@sync-db:3306/syncstorage
SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3306/tokenserver
SYNC_TOKENSERVER__ENABLED: "true"
SYNC_TOKENSERVER__NODE_TYPE: mysql
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net
SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0
SYNC_TOKENSERVER__RUN_MIGRATIONS: "true"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY: "RSA"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG: "RS256"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID: "20190730-15e473fd"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT: "1564502400"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE: "sig"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E: "AQAB"
TOKENSERVER_HOST: http://localhost:8000
SQLALCHEMY_SILENCE_UBER_WARNING: 1
entrypoint: >
/bin/sh -c "
exit_code=0;
PYTHONPATH=/app pytest /app/tools/integration_tests/ --junit-xml=/mysql_integration_results.xml || exit_code=$$?;
export JWK_CACHE_DISABLED=true;
PYTHONPATH=/app pytest /app/tools/integration_tests/ --junit-xml=/mysql_no_jwk_integration_results.xml || exit_code=$$?;
exit $$exit_code;
"

View File

@ -1,47 +0,0 @@
services:
postgres-e2e-tests:
container_name: postgres-e2e-tests
depends_on:
mock-fxa-server:
condition: service_started
syncserver:
condition: service_healthy
sync-db:
condition: service_healthy
tokenserver-db:
condition: service_healthy
image: app:build
privileged: true
user: root
environment:
# Setting this to false will delete any of those keys before starting
# the syncserver and starting the test. This can be set/passed
# in from CircleCI when calling `docker-compose -f docker-compose.e2e.postgres.yaml`
JWK_CACHE_DISABLED: false
MOCK_FXA_SERVER_URL: http://mock-fxa-server:6000
SYNC_HOST: 0.0.0.0
SYNC_MASTER_SECRET: secret0
SYNC_SYNCSTORAGE__DATABASE_URL: postgres://test:test@sync-db:5432/syncstorage
SYNC_TOKENSERVER__DATABASE_URL: postgres://test:test@tokenserver-db:5432/tokenserver
SYNC_TOKENSERVER__ENABLED: "true"
SYNC_TOKENSERVER__NODE_TYPE: postgres
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net
SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0
SYNC_TOKENSERVER__RUN_MIGRATIONS: "true"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY: "RSA"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG: "RS256"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID: "20190730-15e473fd"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT: "1564502400"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE: "sig"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E: "AQAB"
TOKENSERVER_HOST: http://localhost:8000
SQLALCHEMY_SILENCE_UBER_WARNING: 1
entrypoint: >
/bin/sh -c "
exit_code=0;
PYTHONPATH=/app pytest /app/tools/integration_tests/ --junit-xml=/postgres_integration_results.xml || exit_code=$$?;
export JWK_CACHE_DISABLED=true;
PYTHONPATH=/app pytest /app/tools/integration_tests/ --junit-xml=/postgres_no_jwk_integration_results.xml || exit_code=$$?;
exit $$exit_code;
"

View File

@ -1,47 +0,0 @@
services:
spanner-e2e-tests:
container_name: spanner-e2e-tests
depends_on:
mock-fxa-server:
condition: service_started
syncserver:
condition: service_healthy
tokenserver-db:
condition: service_healthy
image: app:build
privileged: true
user: root
environment:
# Some tests can run without the `FXA_OAUTH...` vars.
# Setting this to false will delete any of those keys before starting
# the syncserver and startging the test. This can be set/passed
# in from CircleCI when calling `docker-compose -f docker-compose.e2e.spanner.yaml`
JWK_CACHE_DISABLED: false
MOCK_FXA_SERVER_URL: http://mock-fxa-server:6000
SYNC_HOST: 0.0.0.0
SYNC_MASTER_SECRET: secret0
SYNC_SYNCSTORAGE__DATABASE_URL: spanner://projects/test-project/instances/test-instance/databases/test-database
SYNC_SYNCSTORAGE__SPANNER_EMULATOR_HOST: sync-db:9010
SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3306/tokenserver
SYNC_TOKENSERVER__ENABLED: "true"
SYNC_TOKENSERVER__NODE_TYPE: spanner
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net
SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0
SYNC_TOKENSERVER__RUN_MIGRATIONS: "true"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY: "RSA"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG: "RS256"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID: "20190730-15e473fd"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT: "1564502400"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE: "sig"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E: "AQAB"
TOKENSERVER_HOST: http://localhost:8000
SQLALCHEMY_SILENCE_UBER_WARNING: 1
entrypoint: >
/bin/sh -c "
exit_code=0;
PYTHONPATH=/app pytest /app/tools/integration_tests/ --junit-xml=/spanner_integration_results.xml || exit_code=$$?;
export JWK_CACHE_DISABLED=true;
PYTHONPATH=/app pytest /app/tools/integration_tests/ --junit-xml=/spanner_no_jwk_integration_results.xml || exit_code=$$?;
exit $$exit_code;
"

View File

@ -0,0 +1,19 @@
services:
syncserver:
environment:
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY: "RSA"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG: "RS256"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID: "20190730-15e473fd"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT: "1564502400"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE: "sig"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N: "15OpVGC7ws_SlU0gRbRh1Iwo8_gR8ElX2CDnbN5blKyXLg-ll0ogktoDXc-tDvTabRTxi7AXU0wWQ247odhHT47y5uz0GASYXdfPponynQ_xR9CpNn1eEL1gvDhQN9rfPIzfncl8FUi9V4WMd5f600QC81yDw9dX-Z8gdkru0aDaoEKF9-wU2TqrCNcQdiJCX9BISotjz_9cmGwKXFEekQNJWBeRQxH2bUmgwUK0HaqwW9WbYOs-zstNXXWFsgK9fbDQqQeGehXLZM4Cy5Mgl_iuSvnT3rLzPo2BmlxMLUvRqBx3_v8BTtwmNGA0v9O0FJS_mnDq0Iue0Dz8BssQCQ"
SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E: "AQAB"
SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL: http://mock-fxa-server:6000
e2e-tests:
entrypoint:
- /bin/sh
- -c
- >-
PYTHONPATH=/app
pytest /app/tools/integration_tests/
--junit-xml=/${RESULTS_FILENAME}

View File

@ -0,0 +1,36 @@
services:
syncserver:
environment:
SYNC_TOKENSERVER__ENABLED: "true"
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net
SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0
SYNC_TOKENSERVER__RUN_MIGRATIONS: "true"
e2e-tests:
container_name: mysql-e2e-tests
depends_on:
mock-fxa-server:
condition: service_started
syncserver:
condition: service_healthy
sync-db:
condition: service_healthy
tokenserver-db:
condition: service_healthy
image: app:build
privileged: true
user: root
environment:
SYNC_SERVER_URL: http://syncserver:8000
TOKENSERVER_HOST: http://syncserver:8000
SYNC_MASTER_SECRET: secret0
SYNC_SYNCSTORAGE__DATABASE_URL: mysql://test:test@sync-db:3306/syncstorage
SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3306/tokenserver
SQLALCHEMY_SILENCE_UBER_WARNING: 1
RESULTS_FILENAME: ${RESULTS_FILENAME:-mysql_integration_results.xml}
entrypoint:
- /bin/sh
- -c
- >-
PYTHONPATH=/app
pytest /app/tools/integration_tests/
--junit-xml=/${RESULTS_FILENAME}

View File

@ -0,0 +1,9 @@
services:
e2e-tests:
entrypoint:
- /bin/sh
- -c
- >-
PYTHONPATH=/app
pytest /app/tools/integration_tests/tokenserver/test_e2e.py
--junit-xml=/${RESULTS_FILENAME}

View File

@ -0,0 +1,36 @@
services:
syncserver:
environment:
SYNC_TOKENSERVER__ENABLED: "true"
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net
SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0
SYNC_TOKENSERVER__RUN_MIGRATIONS: "true"
e2e-tests:
container_name: postgres-e2e-tests
depends_on:
mock-fxa-server:
condition: service_started
syncserver:
condition: service_healthy
sync-db:
condition: service_healthy
tokenserver-db:
condition: service_healthy
image: app:build
privileged: true
user: root
environment:
SYNC_SERVER_URL: http://syncserver:8000
TOKENSERVER_HOST: http://syncserver:8000
SYNC_MASTER_SECRET: secret0
SYNC_SYNCSTORAGE__DATABASE_URL: postgres://test:test@sync-db:5432/syncstorage
SYNC_TOKENSERVER__DATABASE_URL: postgres://test:test@tokenserver-db:5432/tokenserver
SQLALCHEMY_SILENCE_UBER_WARNING: 1
RESULTS_FILENAME: ${RESULTS_FILENAME:-postgres_integration_results.xml}
entrypoint:
- /bin/sh
- -c
- >-
PYTHONPATH=/app
pytest /app/tools/integration_tests/
--junit-xml=/${RESULTS_FILENAME}

View File

@ -0,0 +1,35 @@
services:
syncserver:
environment:
SYNC_TOKENSERVER__ENABLED: "true"
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN: api-accounts.stage.mozaws.net
SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET: secret0
SYNC_TOKENSERVER__RUN_MIGRATIONS: "true"
e2e-tests:
container_name: spanner-e2e-tests
depends_on:
mock-fxa-server:
condition: service_started
syncserver:
condition: service_healthy
tokenserver-db:
condition: service_healthy
image: app:build
privileged: true
user: root
environment:
SYNC_SERVER_URL: http://syncserver:8000
TOKENSERVER_HOST: http://syncserver:8000
SYNC_MASTER_SECRET: secret0
SYNC_SYNCSTORAGE__DATABASE_URL: spanner://projects/test-project/instances/test-instance/databases/test-database
SYNC_SYNCSTORAGE__SPANNER_EMULATOR_HOST: sync-db:9010
SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3306/tokenserver
SQLALCHEMY_SILENCE_UBER_WARNING: 1
RESULTS_FILENAME: ${RESULTS_FILENAME:-spanner_integration_results.xml}
entrypoint:
- /bin/sh
- -c
- >-
PYTHONPATH=/app
pytest /app/tools/integration_tests/
--junit-xml=/${RESULTS_FILENAME}

View File

@ -433,14 +433,14 @@ This requires access to [Google Cloud Rust (raw)](https://crates.io/crates/googl
1. Make sure you have [Docker installed](https://docs.docker.com/install/) locally.
2. Copy the contents of mozilla-rust-sdk into top level root dir here.
3. Comment out the `image` value under `syncserver` in either docker-compose.mysql.yml or docker-compose.spanner.yml (depending on which database backend you want to run), and add this instead:
3. Comment out the `image` value under `syncserver` in either docker/docker-compose.mysql.yaml or docker/docker-compose.spanner.yaml (depending on which database backend you want to run), and add this instead:
```yml
build:
context: .
```
4. If you are using MySQL, adjust the MySQL db credentials in docker-compose.mysql.yml to match your local setup.
4. If you are using MySQL, adjust the MySQL db credentials in docker/docker-compose.mysql.yaml to match your local setup.
5. `make docker_start_mysql` or `make docker_start_spanner` - You can verify it's working by visiting [localhost:8000/\_\_heartbeat\_\_](http://localhost:8000/__heartbeat__)
### Connecting to Firefox
@ -477,76 +477,6 @@ If you see a problem related to `libssl` you may need to specify the `cargo` opt
- If you're having trouble working with Sentry to create releases, try authenticating using their self hosted server option that's outlined [here](https://docs.sentry.io/product/cli/configuration/) Ie, `sentry-cli --url https://selfhosted.url.com/ login`. It's also recommended to create a `.sentryclirc` config file. See [this example](https://github.com/mozilla-services/syncstorage-rs/blob/master/.sentryclirc.example) for the config values you'll need.
## Tests
### Unit tests
Run unit tests for a specific database backend using one of the following make targets:
- MySQL: `make test` or `make test_with_coverage`
- Postgres: `make postgres_test_with_coverage`
- Spanner: `make spanner_test_with_coverage`
These commands will run the Rust test suite using cargo-nextest and generate coverage reports using cargo-llvm-cov.
### End-to-End tests
End-to-end (E2E) tests validate the complete integration of syncstorage-rs with a real database backend and mock Firefox Accounts server. These tests run the full Python integration test suite located in [tools/integration_tests/](../../tools/integration_tests/).
#### Running E2E Tests Locally
To run E2E tests, you'll need to:
1. Build a Docker image for your target backend using the appropriate Makefile target
2. Run the E2E test suite using docker-compose
The E2E tests are available for three database backends:
**MySQL:**
```bash
make docker_run_mysql_e2e_tests
```
**Postgres:**
```bash
make docker_run_postgres_e2e_tests
```
**Spanner:**
```bash
make docker_run_spanner_e2e_tests
```
Each E2E test run:
1. Starts the required services (database, mock FxA server, syncserver) using docker-compose
2. Runs the Python integration tests with JWK caching enabled
3. Runs the tests again with JWK caching disabled
4. Outputs JUnit XML test results
The E2E test configurations are defined in:
- [docker-compose.e2e.mysql.yaml](../../docker-compose.e2e.mysql.yaml)
- [docker-compose.e2e.postgres.yaml](../../docker-compose.e2e.postgres.yaml)
- [docker-compose.e2e.spanner.yaml](../../docker-compose.e2e.spanner.yaml)
These compose files extend the base service definitions from their corresponding `docker-compose.<backend>.yaml` files.
#### How E2E Tests Work
The E2E tests:
- Run in a containerized environment with all dependencies (database, syncserver, mock FxA)
- Execute integration tests from [tools/integration_tests/](../../tools/integration_tests/) using pytest
- Test OAuth token validation with both cached and non-cached JWKs
- Validate tokenserver functionality, including user allocation and token generation
- Test syncstorage operations like BSO creation, retrieval, and deletion
#### CI/CD
In GitHub Actions, E2E tests run as part of the CI/CD pipeline for each backend:
- [.github/workflows/mysql.yml](../../.github/workflows/mysql.yml) - `mysql-e2e-tests` job
- [.github/workflows/postgres.yml](../../.github/workflows/postgres.yml) - `postgres-e2e-tests` job
- [.github/workflows/spanner.yml](../../.github/workflows/spanner.yml) - `spanner-e2e-tests` job
Each workflow builds a Docker image, runs unit tests, then executes E2E tests using the same make targets described above.
- [System Requirements](#system-requirements)
- [Local Setup](#local-setup)

View File

@ -73,18 +73,23 @@ make docker_run_postgres_e2e_tests
make docker_run_spanner_e2e_tests
```
Each E2E test run:
1. Starts the required services (database, mock FxA server, syncserver) using docker-compose
2. Runs the Python integration tests with JWK caching enabled
3. Runs the tests again with JWK caching disabled
4. Outputs JUnit XML test results
Each E2E test run performs two separate docker-compose invocations:
1. **No Local JWK run**: starts services with no local JWK configured, runs only `test_e2e.py` against FxA stage
2. **Local JWK & Mocked FxA run**: runs all integration tests using a mocked local FxA server and local JWK; the local JWK affects only the tests in `test_e2e.py`
3. Outputs JUnit XML test results for each run
The E2E test configurations are defined in:
- [docker-compose.e2e.mysql.yaml](../../docker-compose.e2e.mysql.yaml)
- [docker-compose.e2e.postgres.yaml](../../docker-compose.e2e.postgres.yaml)
- [docker-compose.e2e.spanner.yaml](../../docker-compose.e2e.spanner.yaml)
- [docker/docker-compose.e2e.mysql.yaml](../../docker/docker-compose.e2e.mysql.yaml) - base
- [docker/docker-compose.e2e.mysql.jwk-cache.yaml](../../docker/docker-compose.e2e.mysql.jwk-cache.yaml) - JWK + mock FxA overlay
- [docker/docker-compose.e2e.mysql.no-jwk-cache.yaml](../../docker/docker-compose.e2e.mysql.no-jwk-cache.yaml) - FxA stage overlay
- [docker/docker-compose.e2e.postgres.yaml](../../docker/docker-compose.e2e.postgres.yaml)
- [docker/docker-compose.e2e.postgres.jwk-cache.yaml](../../docker/docker-compose.e2e.postgres.jwk-cache.yaml)
- [docker/docker-compose.e2e.postgres.no-jwk-cache.yaml](../../docker/docker-compose.e2e.postgres.no-jwk-cache.yaml)
- [docker/docker-compose.e2e.spanner.yaml](../../docker/docker-compose.e2e.spanner.yaml)
- [docker/docker-compose.e2e.spanner.jwk-cache.yaml](../../docker/docker-compose.e2e.spanner.jwk-cache.yaml)
- [docker/docker-compose.e2e.spanner.no-jwk-cache.yaml](../../docker/docker-compose.e2e.spanner.no-jwk-cache.yaml)
These compose files extend the base service definitions from their corresponding `docker-compose.<backend>.yaml` files.
These compose files extend the base service definitions from their corresponding `docker/docker-compose.<backend>.yaml` files. Syncserver configuration (JWK, FxA OAuth URL, CORS) is defined in the `syncserver` block of the e2e overlays.
#### How E2E Tests Work

View File

@ -1,136 +0,0 @@
"""Pytest configuration and fixtures for integration tests."""
import os
import psutil
import signal
import subprocess
import time
import pytest
import requests # type: ignore[import-untyped]
import logging
DEBUG_BUILD = "target/debug/syncserver"
RELEASE_BUILD = "/app/bin/syncserver"
# max number of attempts to check server heartbeat
SYNC_SERVER_STARTUP_MAX_ATTEMPTS = 35
JWK_CACHE_DISABLED = os.environ.get("JWK_CACHE_DISABLED")
logger = logging.getLogger("tokenserver.scripts.conftest")
# Local setup for fixtures
def _terminate_process(process):
"""Gracefully terminate the process and its children."""
proc = psutil.Process(pid=process.pid)
child_proc = proc.children(recursive=True)
for p in [proc] + child_proc:
os.kill(p.pid, signal.SIGTERM)
process.wait()
def _wait_for_server_startup(max_attempts=SYNC_SERVER_STARTUP_MAX_ATTEMPTS):
"""Wait for the __heartbeat__ endpoint to return a 200.
Pause for 1 second between attempts. Raise a RuntimeError if the server
does not start after the specific number of attempts.
"""
itter = 0
while True:
if itter >= max_attempts:
raise RuntimeError("Server failed to start within the timeout period.")
try:
req = requests.get("http://localhost:8000/__heartbeat__", timeout=2)
if req.status_code == 200:
break
except requests.exceptions.RequestException as e:
logger.warning("Connection failed: %s", e)
time.sleep(1)
itter += 1
def _start_server():
"""Start the syncserver process, wait for it to be running, and return the handle."""
target_binary = None
if os.path.exists(DEBUG_BUILD):
target_binary = DEBUG_BUILD
elif os.path.exists(RELEASE_BUILD):
target_binary = RELEASE_BUILD
else:
raise RuntimeError("Neither {DEBUG_BUILD} nor {RELEASE_BUILD} were found.")
server_proc = subprocess.Popen(
[target_binary],
text=True,
env=os.environ,
)
_wait_for_server_startup()
return server_proc
def _server_manager():
"""Gracefully start and stop the server as a context manager."""
server_process = _start_server()
try:
yield server_process
finally:
_terminate_process(server_process)
def _set_local_test_env_vars():
"""Set environment variables for local testing.
This function sets the necessary environment variables for the syncserver.
"""
os.environ.setdefault("SYNC_MASTER_SECRET", "secret0")
os.environ.setdefault("SYNC_CORS_MAX_AGE", "555")
os.environ.setdefault("SYNC_CORS_ALLOWED_ORIGIN", "*")
os.environ["MOZSVC_TEST_REMOTE"] = "localhost"
os.environ["SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL"] = os.environ[
"MOCK_FXA_SERVER_URL"
]
# Fixtures
@pytest.fixture(scope="session")
def setup_server_local_testing():
"""Set up the server for local testing.
This fixture sets the necessary environment variables and
starts the server.
"""
_set_local_test_env_vars()
yield from _server_manager()
@pytest.fixture(scope="session")
def setup_server_end_to_end_testing():
"""Set up the server for end-to-end testing.
This fixture sets the necessary environment variables and
starts the server.
"""
_set_local_test_env_vars()
# debatable if this should ONLY be here since it was only
# done against the "run_end_to_end_tests" prior, of if we
# just do it in _set_local_test_env_vars...
if JWK_CACHE_DISABLED:
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KTY"]
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__ALG"]
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__KID"]
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__FXA_CREATED_AT"]
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__USE"]
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__N"]
del os.environ["SYNC_TOKENSERVER__FXA_OAUTH_PRIMARY_JWK__E"]
# Set OAuth-specific environment variables
os.environ["SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL"] = (
"https://oauth.stage.mozaws.net"
)
# Start the server
yield from _server_manager()

View File

@ -76,7 +76,6 @@ def randtext(size=10):
return "".join([random.choice(_ASCII) for i in range(size)])
@pytest.mark.usefixtures("setup_server_local_testing")
class TestStorage(StorageFunctionalTestCase):
"""Storage testcases that only use the web API.
@ -2311,7 +2310,7 @@ class TestStorage(StorageFunctionalTestCase):
},
)
self.assertEqual(int(res.headers["access-control-max-age"]), 555)
self.assertGreater(int(res.headers["access-control-max-age"]), 0)
self.assertEqual(res.headers["access-control-allow-origin"], "localhost")
def test_cors_allows_any_origin(self):

View File

@ -358,7 +358,7 @@ class FunctionalTestCase(TestCase):
# delete the ones that don't work with distant = True along
# with the need for self.distant.
self.distant = False
self.host_url = "http://localhost:8000"
self.host_url = os.environ.get("SYNC_SERVER_URL", "http://localhost:8000")
# This call implicitly commits the configurator. We probably still
# want it for the side effects.
self.config.make_wsgi_app()

View File

@ -3,12 +3,10 @@
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Authorization integration tests for the tokenserver."""
import pytest
import unittest
from integration_tests.tokenserver.test_support import TestCase
@pytest.mark.usefixtures("setup_server_local_testing")
class TestAuthorization(TestCase, unittest.TestCase):
"""Authorization integration tests for the tokenserver."""

View File

@ -7,7 +7,6 @@ from base64 import urlsafe_b64decode
import hmac
import json
import jwt
import pytest
import random
import string
import time
@ -36,7 +35,6 @@ PASSWORD_LENGTH = 32
SCOPE = "https://identity.mozilla.com/apps/oldsync"
@pytest.mark.usefixtures("setup_server_end_to_end_testing")
class TestE2e(TestCase, unittest.TestCase):
"""End-to-end integration tests using real FxA accounts."""

View File

@ -3,7 +3,6 @@
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Miscellaneous integration tests for the tokenserver."""
import pytest
import unittest
from integration_tests.tokenserver.test_support import TestCase
@ -11,7 +10,6 @@ from integration_tests.tokenserver.test_support import TestCase
MAX_GENERATION = 9223372036854775807
@pytest.mark.usefixtures("setup_server_local_testing")
class TestMisc(TestCase, unittest.TestCase):
"""Miscellaneous tokenserver integration tests."""

View File

@ -3,14 +3,12 @@
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Node assignment integration tests for the tokenserver."""
import pytest
import unittest
from integration_tests.tokenserver.test_support import TestCase
from sqlalchemy.sql import text as sqltext
@pytest.mark.usefixtures("setup_server_local_testing")
class TestNodeAssignment(TestCase, unittest.TestCase):
"""Node assignment integration tests for the tokenserver."""