diff --git a/.circleci/config.yml b/.circleci/config.yml index c63fce7812..369c792123 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -62,6 +62,18 @@ commands: - run: name: Install grpcio dependencies command: sudo apt-get update && sudo apt-get install -y cmake golang-go + setup-mysql: + steps: + - run: + name: Install MySQL client + command: sudo apt-get update && sudo apt-get install -y default-mysql-client + create-tokenserver-database: + steps: + - run: + name: Create Tokenserver database + command: | + mysql -u root -ppassword -h 127.0.0.1 -e 'CREATE DATABASE tokenserver;' + mysql -u root -ppassword -h 127.0.0.1 -e "GRANT ALL ON tokenserver.* to 'test'@'%';" write-version: steps: @@ -163,6 +175,7 @@ jobs: password: $DOCKER_PASS environment: SYNC_DATABASE_URL: mysql://test:test@127.0.0.1/syncstorage + SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@127.0.0.1/tokenserver RUST_BACKTRACE: 1 # XXX: begin_test_transaction doesn't play nice over threaded tests RUST_TEST_THREADS: 1 @@ -171,7 +184,7 @@ jobs: username: $DOCKER_USER password: $DOCKER_PASS environment: - MYSQL_ROOT_PASSWORD: random + MYSQL_ROOT_PASSWORD: password MYSQL_USER: test MYSQL_PASSWORD: test MYSQL_DATABASE: syncstorage @@ -190,6 +203,8 @@ jobs: - setup-rust - setup-python - setup-gcp-grpc + - setup-mysql + - create-tokenserver-database # XXX: currently the time needed to setup-sccache negates its savings #- setup-sccache #- restore-sccache-cache diff --git a/Makefile b/Makefile index 274402f090..c2c5f9484a 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ ## SYNC_DATABASE_URL = 'mysql://sample_user:sample_password@localhost/syncstorage_rs' +SYNC_TOKENSERVER__DATABASE_URL = 'mysql://sample_user:sample_password@localhost/tokenserver_rs' # This key can live anywhere on your machine. Adjust path as needed. PATH_TO_SYNC_SPANNER_KEYS = `pwd`/service-account.json @@ -41,4 +42,4 @@ run_spanner: GOOGLE_APPLICATION_CREDENTIALS=$(PATH_TO_SYNC_SPANNER_KEYS) GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=$(PATH_TO_GRPC_CERT) make run test: - SYNC_DATABASE_URL=$(SYNC_DATABASE_URL) RUST_TEST_THREADS=1 cargo test + SYNC_DATABASE_URL=$(SYNC_DATABASE_URL) SYNC_TOKENSERVER__DATABASE_URL=$(SYNC_TOKENSERVER__DATABASE_URL) RUST_TEST_THREADS=1 cargo test diff --git a/docker-compose.e2e.mysql.yaml b/docker-compose.e2e.mysql.yaml index 437761a2ca..318f87ab23 100644 --- a/docker-compose.e2e.mysql.yaml +++ b/docker-compose.e2e.mysql.yaml @@ -1,9 +1,11 @@ version: '3' services: - db: + sync-db: + tokenserver-db: syncstorage-rs: depends_on: - - db + - sync-db + - tokenserver-db # TODO: either syncstorage-rs should retry the db connection # itself a few times or should include a wait-for-it.sh script # inside its docker that would do this for us. Same (probably @@ -22,11 +24,8 @@ services: environment: SYNC_HOST: 0.0.0.0 SYNC_MASTER_SECRET: secret0 - SYNC_DATABASE_URL: mysql://test:test@db:3306/syncstorage - SYNC_TOKENSERVER_DATABASE_URL: mysql://username:pw@localhost/tokenserver - SYNC_TOKENSERVER_JWKS_RSA_MODULUS: 2lDphW0lNZ4w1m9CfmIhC1AxYG9iwihxBdQZo7_6e0TBAi8_TNaoHHI90G9n5d8BQQnNcF4j2vOs006zlXcqGrP27b49KkN3FmbcOMovvfesMseghaqXqqFLALL9us3Wstt_fV_qV7ceRcJq5Hd_Mq85qUgYSfb9qp0vyePb26KEGy4cwO7c9nCna1a_i5rzUEJu6bAtcLS5obSvmsOOpTLHXojKKOnC4LRC3osdR6AU6v3UObKgJlkk_-8LmPhQZqOXiI_TdBpNiw6G_-eishg8V_poPlAnLNd8mfZBam-_7CdUS4-YoOvJZfYjIoboOuVmUrBjogFyDo72EPTReQ - SYNC_TOKENSERVER_JWKS_RSA_EXPONENT: AQAB - SYNC_FXA_METRICS_HASH_SECRET: insecure + SYNC_DATABASE_URL: mysql://test:test@sync-db:3306/syncstorage + SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3307/tokenserver entrypoint: > /bin/sh -c " sleep 28; pip3 install -r /app/tools/integration_tests/requirements.txt && python3 /app/tools/integration_tests/run.py 'http://localhost:8000#secret0' diff --git a/docker-compose.e2e.spanner.yaml b/docker-compose.e2e.spanner.yaml index 15f28b594a..05b6044d95 100644 --- a/docker-compose.e2e.spanner.yaml +++ b/docker-compose.e2e.spanner.yaml @@ -1,10 +1,11 @@ version: '3' services: - db: - db-setup: + sync-db: + sync-db-setup: + tokenserver-db: syncstorage-rs: depends_on: - - db-setup + - sync-db-setup # TODO: either syncstorage-rs should retry the db connection # itself a few times or should include a wait-for-it.sh script # inside its docker that would do this for us. Same (probably @@ -24,11 +25,8 @@ services: SYNC_HOST: 0.0.0.0 SYNC_MASTER_SECRET: secret0 SYNC_DATABASE_URL: spanner://projects/test-project/instances/test-instance/databases/test-database - SYNC_SPANNER_EMULATOR_HOST: db:9010 - SYNC_TOKENSERVER_DATABASE_URL: mysql://username:pw@localhost/tokenserver - SYNC_TOKENSERVER_JWKS_RSA_MODULUS: 2lDphW0lNZ4w1m9CfmIhC1AxYG9iwihxBdQZo7_6e0TBAi8_TNaoHHI90G9n5d8BQQnNcF4j2vOs006zlXcqGrP27b49KkN3FmbcOMovvfesMseghaqXqqFLALL9us3Wstt_fV_qV7ceRcJq5Hd_Mq85qUgYSfb9qp0vyePb26KEGy4cwO7c9nCna1a_i5rzUEJu6bAtcLS5obSvmsOOpTLHXojKKOnC4LRC3osdR6AU6v3UObKgJlkk_-8LmPhQZqOXiI_TdBpNiw6G_-eishg8V_poPlAnLNd8mfZBam-_7CdUS4-YoOvJZfYjIoboOuVmUrBjogFyDo72EPTReQ - SYNC_TOKENSERVER_JWKS_RSA_EXPONENT: AQAB - SYNC_FXA_METRICS_HASH_SECRET: insecure + SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3306/tokenserver + SYNC_SPANNER_EMULATOR_HOST: sync-db:9010 entrypoint: > /bin/sh -c " sleep 28; pip3 install -r /app/tools/integration_tests/requirements.txt && python3 /app/tools/integration_tests/run.py 'http://localhost:8000#secret0' diff --git a/docker-compose.mysql.yaml b/docker-compose.mysql.yaml index 141d9794ff..840bf39483 100644 --- a/docker-compose.mysql.yaml +++ b/docker-compose.mysql.yaml @@ -1,9 +1,9 @@ version: '3' services: - db: + sync-db: image: mysql:5.7 volumes: - - db_data:/var/lib/mysql + - sync_db_data:/var/lib/mysql restart: always ports: - "3306" @@ -14,24 +14,37 @@ services: MYSQL_USER: test MYSQL_PASSWORD: test + tokenserver-db: + image: mysql:5.7 + volumes: + - tokenserver_db_data:/var/lib/mysql + restart: always + ports: + - "3307" + environment: + #MYSQL_RANDOM_ROOT_PASSWORD: yes + MYSQL_ROOT_PASSWORD: random + MYSQL_DATABASE: tokenserver + MYSQL_USER: test + MYSQL_PASSWORD: test + syncstorage-rs: image: ${SYNCSTORAGE_RS_IMAGE:-syncstorage-rs:latest} restart: always ports: - "8000:8000" depends_on: - - db + - sync-db + - tokenserver-db environment: SYNC_HOST: 0.0.0.0 SYNC_MASTER_SECRET: secret0 - SYNC_DATABASE_URL: mysql://test:test@db:3306/syncstorage - SYNC_TOKENSERVER_DATABASE_URL: mysql://username:pw@localhost/tokenserver - SYNC_TOKENSERVER_JWKS_RSA_MODULUS: 2lDphW0lNZ4w1m9CfmIhC1AxYG9iwihxBdQZo7_6e0TBAi8_TNaoHHI90G9n5d8BQQnNcF4j2vOs006zlXcqGrP27b49KkN3FmbcOMovvfesMseghaqXqqFLALL9us3Wstt_fV_qV7ceRcJq5Hd_Mq85qUgYSfb9qp0vyePb26KEGy4cwO7c9nCna1a_i5rzUEJu6bAtcLS5obSvmsOOpTLHXojKKOnC4LRC3osdR6AU6v3UObKgJlkk_-8LmPhQZqOXiI_TdBpNiw6G_-eishg8V_poPlAnLNd8mfZBam-_7CdUS4-YoOvJZfYjIoboOuVmUrBjogFyDo72EPTReQ - SYNC_TOKENSERVER_JWKS_RSA_EXPONENT: AQAB - SYNC_FXA_METRICS_HASH_SECRET: insecure + SYNC_DATABASE_URL: mysql://test:test@sync-db:3306/syncstorage + SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3307/tokenserver volumes: - db_data: + sync_db_data: + tokenserver_db_data: # Application runs off of port 8000. # you can test if it's available with diff --git a/docker-compose.spanner.yaml b/docker-compose.spanner.yaml index 7df769935b..903d464b85 100644 --- a/docker-compose.spanner.yaml +++ b/docker-compose.spanner.yaml @@ -1,39 +1,49 @@ version: '3' services: - db: + sync-db: image: gcr.io/cloud-spanner-emulator/emulator ports: - "9010:9010" - "9020:9020" environment: PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - db-setup: + sync-db-setup: image: app:build depends_on: - - db + - sync-db restart: "no" entrypoint: "/app/scripts/prepare-spanner.sh" environment: - SYNC_SPANNER_EMULATOR_HOST: db:9020 + SYNC_SPANNER_EMULATOR_HOST: sync-db:9020 + tokenserver-db: + image: mysql:5.7 + volumes: + - tokenserver_db_data:/var/lib/mysql + restart: always + ports: + - "3306" + environment: + #MYSQL_RANDOM_ROOT_PASSWORD: yes + MYSQL_ROOT_PASSWORD: random + MYSQL_DATABASE: tokenserver + MYSQL_USER: test + MYSQL_PASSWORD: test syncstorage-rs: image: ${SYNCSTORAGE_RS_IMAGE:-syncstorage-rs:latest} restart: always ports: - "8000:8000" depends_on: - - db-setup + - sync-db-setup environment: SYNC_HOST: 0.0.0.0 SYNC_MASTER_SECRET: secret0 SYNC_DATABASE_URL: spanner://projects/test-project/instances/test-instance/databases/test-database - SYNC_SPANNER_EMULATOR_HOST: db:9010 - SYNC_TOKENSERVER_DATABASE_URL: mysql://username:pw@localhost/tokenserver - SYNC_TOKENSERVER_JWKS_RSA_MODULUS: 2lDphW0lNZ4w1m9CfmIhC1AxYG9iwihxBdQZo7_6e0TBAi8_TNaoHHI90G9n5d8BQQnNcF4j2vOs006zlXcqGrP27b49KkN3FmbcOMovvfesMseghaqXqqFLALL9us3Wstt_fV_qV7ceRcJq5Hd_Mq85qUgYSfb9qp0vyePb26KEGy4cwO7c9nCna1a_i5rzUEJu6bAtcLS5obSvmsOOpTLHXojKKOnC4LRC3osdR6AU6v3UObKgJlkk_-8LmPhQZqOXiI_TdBpNiw6G_-eishg8V_poPlAnLNd8mfZBam-_7CdUS4-YoOvJZfYjIoboOuVmUrBjogFyDo72EPTReQ - SYNC_TOKENSERVER_JWKS_RSA_EXPONENT: AQAB - SYNC_FXA_METRICS_HASH_SECRET: insecure + SYNC_SPANNER_EMULATOR_HOST: sync-db:9010 + SYNC_TOKENSERVER__DATABASE_URL: mysql://test:test@tokenserver-db:3306/tokenserver volumes: - db_data: + tokenserver_db_data: # Application runs off of port 8000. # you can test if it's available with diff --git a/src/db/mysql/mod.rs b/src/db/mysql/mod.rs index ba73ef0e4c..82ea3a9b7b 100644 --- a/src/db/mysql/mod.rs +++ b/src/db/mysql/mod.rs @@ -8,3 +8,5 @@ mod schema; mod test; pub use self::pool::MysqlDbPool; +#[cfg(test)] +pub use self::test::TestTransactionCustomizer; diff --git a/src/db/mysql/models.rs b/src/db/mysql/models.rs index 4fdec0c350..02851332d6 100644 --- a/src/db/mysql/models.rs +++ b/src/db/mysql/models.rs @@ -994,6 +994,7 @@ impl MysqlDb { } } +#[macro_export] macro_rules! sync_db_method { ($name:ident, $sync_name:ident, $type:ident) => { sync_db_method!($name, $sync_name, $type, results::$type); diff --git a/src/db/mysql/pool.rs b/src/db/mysql/pool.rs index e60743d036..d179604860 100644 --- a/src/db/mysql/pool.rs +++ b/src/db/mysql/pool.rs @@ -35,8 +35,8 @@ embed_migrations!(); /// /// Mysql DDL statements implicitly commit which could disrupt MysqlPool's /// begin_test_transaction during tests. So this runs on its own separate conn. -pub fn run_embedded_migrations(settings: &Settings) -> Result<()> { - let conn = MysqlConnection::establish(&settings.database_url)?; +pub fn run_embedded_migrations(database_url: &str) -> Result<()> { + let conn = MysqlConnection::establish(database_url)?; #[cfg(test)] // XXX: this doesn't show the DDL statements // https://github.com/shssoichiro/diesel-logger/issues/1 @@ -63,7 +63,7 @@ impl MysqlDbPool { /// /// Also initializes the Mysql db, ensuring all migrations are ran. pub fn new(settings: &Settings, metrics: &Metrics) -> Result { - run_embedded_migrations(settings)?; + run_embedded_migrations(&settings.database_url)?; Self::new_without_migrations(settings, metrics) } diff --git a/src/server/mod.rs b/src/server/mod.rs index f291f9f607..92feb7881d 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -14,7 +14,7 @@ use crate::db::{pool_from_settings, spawn_pool_periodic_reporter, DbPool}; use crate::error::ApiError; use crate::server::metrics::Metrics; use crate::settings::{Deadman, Secrets, ServerLimits, Settings}; -use crate::tokenserver::{self, OAuthVerifier, VerifyToken}; +use crate::tokenserver; use crate::web::{handlers, middleware}; pub const BSO_ID_REGEX: &str = r"[ -~]{1,64}"; @@ -41,12 +41,10 @@ pub struct ServerState { /// Secrets used during Hawk authentication. pub secrets: Arc, - // TODO: These will eventually be added as settings passed to a more mature - // database adapter (which will be added in #1054) - pub tokenserver_database_url: Option, - pub fxa_metrics_hash_secret: Option, // SYNC_FXA_METRICS_HASH_SECRET - - pub tokenserver_oauth_verifier: Box, + // XXX: This is only any Option temporarily. Once Tokenserver is rolled out to production, + // it will always be enabled, and syncstorage will always have state associated with + // Tokenserver. + pub tokenserver_state: Option, /// Metric reporting pub metrics: Box, @@ -140,11 +138,12 @@ macro_rules! build_app { .route(web::get().to(handlers::get_bso)) .route(web::put().to(handlers::put_bso)), ) + // XXX: This route will be enabled when we are ready to roll out Tokenserver // Tokenserver - .service( - web::resource("/1.0/sync/1.5".to_string()) - .route(web::get().to(tokenserver::handlers::get_tokenserver_result)), - ) + // .service( + // web::resource("/1.0/sync/1.5".to_string()) + // .route(web::get().to(tokenserver::handlers::get_tokenserver_result)), + // ) // Dockerflow // Remember to update .::web::middleware::DOCKER_FLOW_ENDPOINTS // when applying changes to endpoint names. @@ -182,15 +181,19 @@ impl Server { let secrets = Arc::new(settings.master_secret); let host = settings.host.clone(); let port = settings.port; - let tokenserver_database_url = Arc::new(settings.tokenserver_database_url.clone()); - let fxa_oauth_server_url = settings.fxa_oauth_server_url; - let fxa_metrics_hash_secret = Arc::new(settings.fxa_metrics_hash_secret.clone()); let quota_enabled = settings.enable_quota; let actix_keep_alive = settings.actix_keep_alive; let deadman = Arc::new(RwLock::new(Deadman { max_size: settings.database_pool_max_size, ..Default::default() })); + let tokenserver_state = if settings.tokenserver.enabled { + Some(tokenserver::ServerState::from_settings( + &settings.tokenserver, + )?) + } else { + None + }; spawn_pool_periodic_reporter(Duration::from_secs(10), metrics.clone(), db_pool.clone())?; @@ -201,11 +204,7 @@ impl Server { limits: Arc::clone(&limits), limits_json: limits_json.clone(), secrets: Arc::clone(&secrets), - tokenserver_database_url: (*tokenserver_database_url).clone(), - fxa_metrics_hash_secret: (*fxa_metrics_hash_secret).clone(), - tokenserver_oauth_verifier: Box::new(OAuthVerifier { - fxa_oauth_server_url: fxa_oauth_server_url.clone(), - }), + tokenserver_state: tokenserver_state.clone(), metrics: Box::new(metrics.clone()), port, quota_enabled, diff --git a/src/server/test.rs b/src/server/test.rs index d75fa57e2c..37b621d539 100644 --- a/src/server/test.rs +++ b/src/server/test.rs @@ -24,7 +24,6 @@ use crate::db::pool_from_settings; use crate::db::results::{DeleteBso, GetBso, PostBsos, PutBso}; use crate::db::util::SyncTimestamp; use crate::settings::{test_settings, Secrets, ServerLimits}; -use crate::tokenserver::MockOAuthVerifier; use crate::web::{auth::HawkPayload, extractors::BsoBody, X_LAST_MODIFIED}; lazy_static! { @@ -70,9 +69,7 @@ async fn get_test_state(settings: &Settings) -> ServerState { limits: Arc::clone(&SERVER_LIMITS), limits_json: serde_json::to_string(&**SERVER_LIMITS).unwrap(), secrets: Arc::clone(&SECRETS), - tokenserver_database_url: None, - fxa_metrics_hash_secret: None, - tokenserver_oauth_verifier: Box::new(MockOAuthVerifier::default()), + tokenserver_state: None, metrics: Box::new(metrics), port: settings.port, quota_enabled: settings.enable_quota, diff --git a/src/settings.rs b/src/settings.rs index 28bf5226a3..07fa2c5aa2 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -7,6 +7,7 @@ use url::Url; use crate::db::spanner::models::MAX_SPANNER_LOAD_SIZE; use crate::error::ApiError; +use crate::tokenserver::settings::Settings as TokenserverSettings; use crate::web::auth::hkdf_expand_32; static DEFAULT_PORT: u16 = 8000; @@ -46,7 +47,6 @@ pub struct Settings { pub port: u16, pub host: String, pub database_url: String, - pub tokenserver_database_url: Option, pub database_pool_max_size: Option, // NOTE: Not supported by deadpool! pub database_pool_min_idle: Option, @@ -69,7 +69,6 @@ pub struct Settings { /// that are used during Hawk authentication. pub master_secret: Secrets, - pub fxa_metrics_hash_secret: Option, pub human_logs: bool, pub statsd_host: Option, @@ -81,8 +80,8 @@ pub struct Settings { pub spanner_emulator_host: Option, - /// The URL of the FxA server used for verifying Tokenserver OAuth tokens - pub fxa_oauth_server_url: Option, + /// Settings specific to Tokenserver + pub tokenserver: TokenserverSettings, } impl Default for Settings { @@ -92,7 +91,6 @@ impl Default for Settings { port: DEFAULT_PORT, host: "127.0.0.1".to_string(), database_url: "mysql://root@127.0.0.1/syncstorage".to_string(), - tokenserver_database_url: None, database_pool_max_size: None, database_pool_min_idle: None, database_pool_connection_lifespan: None, @@ -103,7 +101,6 @@ impl Default for Settings { actix_keep_alive: None, limits: ServerLimits::default(), master_secret: Secrets::default(), - fxa_metrics_hash_secret: None, statsd_host: None, statsd_port: 8125, statsd_label: "syncstorage".to_string(), @@ -111,7 +108,7 @@ impl Default for Settings { enable_quota: false, enforce_quota: false, spanner_emulator_host: None, - fxa_oauth_server_url: None, + tokenserver: TokenserverSettings::default(), } } } @@ -137,8 +134,6 @@ impl Settings { // Each backend does their own default process, so specifying a "universal" value // for database_pool_max_size doesn't quite work. Generally the max pool size is // 10. - s.set_default::>("tokenserver_database_url", None)?; - s.set_default::>("fxa_metrics_hash_secret", None)?; s.set_default("master_secret", "")?; s.set_default("limits.max_post_bytes", i64::from(DEFAULT_MAX_POST_BYTES))?; s.set_default( @@ -166,6 +161,15 @@ impl Settings { s.set_default("enable_quota", false)?; s.set_default("enforce_quota", false)?; + // Set Tokenserver defaults + s.set_default( + "tokenserver.database_url", + "mysql://root@127.0.0.1/tokenserver", + )?; + s.set_default("tokenserver.enabled", false)?; + s.set_default("tokenserver.fxa_email_domain", "test.com")?; + s.set_default("tokenserver.fxa_metrics_hash_secret", "secret")?; + // Merge the config file if supplied if let Some(config_filename) = filename { s.merge(File::with_name(config_filename))?; diff --git a/src/tokenserver/db/mock.rs b/src/tokenserver/db/mock.rs new file mode 100644 index 0000000000..8a4c9b6431 --- /dev/null +++ b/src/tokenserver/db/mock.rs @@ -0,0 +1,58 @@ +#![allow(clippy::new_without_default)] + +use futures::future; + +use super::models::{Db, DbFuture}; +#[cfg(test)] +use super::params; +use super::pool::DbPool; +use super::results; +use crate::db::error::DbError; + +#[derive(Clone, Debug)] +pub struct MockDbPool; + +impl MockDbPool { + pub fn new() -> Self { + MockDbPool + } +} + +impl DbPool for MockDbPool { + fn get(&self) -> Result, DbError> { + Ok(Box::new(MockDb::new())) + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +#[derive(Clone, Debug)] +pub struct MockDb; + +impl MockDb { + pub fn new() -> Self { + MockDb + } +} + +impl Db for MockDb { + fn get_user(&self, _email: String) -> DbFuture<'_, results::GetUser> { + Box::pin(future::ok(results::GetUser::default())) + } + + #[cfg(test)] + fn post_node(&self, _node: params::PostNode) -> DbFuture<'_, results::PostNode> { + Box::pin(future::ok(results::PostNode::default())) + } + + #[cfg(test)] + fn post_service(&self, _service: params::PostService) -> DbFuture<'_, results::PostService> { + Box::pin(future::ok(results::PostService::default())) + } + #[cfg(test)] + fn post_user(&self, _user: params::PostUser) -> DbFuture<'_, results::PostUser> { + Box::pin(future::ok(results::PostUser::default())) + } +} diff --git a/src/tokenserver/db/mod.rs b/src/tokenserver/db/mod.rs index 36e130eac6..2bea86bea1 100644 --- a/src/tokenserver/db/mod.rs +++ b/src/tokenserver/db/mod.rs @@ -1,2 +1,5 @@ +pub mod mock; pub mod models; +mod params; +pub mod pool; pub mod results; diff --git a/src/tokenserver/db/models.rs b/src/tokenserver/db/models.rs index bb3e5a193a..06df9303b1 100644 --- a/src/tokenserver/db/models.rs +++ b/src/tokenserver/db/models.rs @@ -1,37 +1,273 @@ -use diesel::mysql::MysqlConnection; -use diesel::sql_types::Text; -use diesel::{Connection, RunQueryDsl}; +use actix_web::web::block; +#[cfg(test)] +use diesel::sql_types::{Bigint, Integer, Nullable}; +use diesel::{ + mysql::MysqlConnection, + r2d2::{ConnectionManager, PooledConnection}, + sql_types::Text, + RunQueryDsl, +}; +#[cfg(test)] +use diesel_logger::LoggingConnection; +use futures::future::LocalBoxFuture; +use futures::TryFutureExt; -use super::results::GetTokenserverUser; +use std::{self, sync::Arc}; + +use super::{params, results}; use crate::db::error::{DbError, DbErrorKind}; +use crate::error::ApiError; +use crate::sync_db_method; + +pub type DbFuture<'a, T> = LocalBoxFuture<'a, Result>; +pub type DbResult = std::result::Result; +type Conn = PooledConnection>; + +#[derive(Clone)] +pub struct TokenserverDb { + /// Synchronous Diesel calls are executed in actix_web::web::block to satisfy + /// the Db trait's asynchronous interface. + /// + /// Arc provides a Clone impl utilized for safely moving to + /// the thread pool but does not provide Send as the underlying db + /// conn. structs are !Sync (Arc requires both for Send). See the Send impl + /// below. + pub(super) inner: Arc, +} + +/// Despite the db conn structs being !Sync (see Arc above) we +/// don't spawn multiple MysqlDb calls at a time in the thread pool. Calls are +/// queued to the thread pool via Futures, naturally serialized. +unsafe impl Send for TokenserverDb {} + +pub struct DbInner { + #[cfg(not(test))] + pub(super) conn: Conn, + #[cfg(test)] + pub(super) conn: LoggingConnection, // display SQL when RUST_LOG="diesel_logger=trace" +} + +impl TokenserverDb { + pub fn new(conn: Conn) -> Self { + let inner = DbInner { + #[cfg(not(test))] + conn, + #[cfg(test)] + conn: LoggingConnection::new(conn), + }; + + Self { + inner: Arc::new(inner), + } + } + + fn get_user_sync(&self, email: String) -> DbResult { + let query = r#" + SELECT users.uid, users.email, users.client_state, users.generation, + users.keys_changed_at, users.created_at, nodes.node + FROM users + JOIN nodes + ON nodes.id = users.nodeid + WHERE users.email = ? + "#; + let mut user_records = diesel::sql_query(query) + .bind::(email) + .load::(&self.inner.conn)?; + + if user_records.is_empty() { + return Err(DbErrorKind::TokenserverUserNotFound.into()); + } + + user_records.sort_by_key(|user_record| (user_record.generation, user_record.created_at)); + let user_record = user_records[0].clone(); -// TODO: Connecting to the database like this is only temporary. In #1054, we -// will add a more mature database adapter for Tokenserver. -pub fn get_tokenserver_user_sync( - email: &str, - database_url: &str, -) -> Result { - let connection = MysqlConnection::establish(database_url) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)); - - let mut user_records = diesel::sql_query( - r#" - SELECT users.uid, users.email, users.client_state, users.generation, - users.keys_changed_at, users.created_at, nodes.node - FROM users - JOIN nodes ON nodes.id = users.nodeid - WHERE users.email = ? - "#, - ) - .bind::(&email) - .load::(&connection)?; - - if user_records.is_empty() { - return Err(DbErrorKind::TokenserverUserNotFound.into()); + Ok(user_record) } - user_records.sort_by_key(|user_record| (user_record.generation, user_record.created_at)); - let user_record = user_records[0].clone(); + #[cfg(test)] + fn post_node_sync(&self, node: params::PostNode) -> DbResult { + let query = r#" + INSERT INTO nodes (service, node, available, current_load, capacity, downed, backoff) + VALUES (?, ?, ?, ?, ?, ?, ?) + "#; + diesel::sql_query(query) + .bind::(node.service_id) + .bind::(&node.node) + .bind::(node.available) + .bind::(node.current_load) + .bind::(node.capacity) + .bind::(node.downed) + .bind::(node.backoff) + .execute(&self.inner.conn)?; + + let query = r#" + SELECT id + FROM nodes + WHERE service = ? + AND node = ? + AND available = ? + AND current_load = ? + AND capacity = ? + AND downed = ? + AND backoff = ? + "#; + diesel::sql_query(query) + .bind::(node.service_id) + .bind::(&node.node) + .bind::(node.available) + .bind::(node.current_load) + .bind::(node.capacity) + .bind::(node.downed) + .bind::(node.backoff) + .get_result::(&self.inner.conn) + .map_err(Into::into) + } + + #[cfg(test)] + fn post_service_sync(&self, service: params::PostService) -> DbResult { + let query = r#" + INSERT INTO services (service, pattern) + VALUES (?, ?) + "#; + diesel::sql_query(query) + .bind::(&service.service) + .bind::(service.pattern) + .execute(&self.inner.conn)?; + + let query = r#" + SELECT id + FROM services + WHERE service = ? + AND pattern = ? + "#; + diesel::sql_query(query) + .bind::(&service.service) + .bind::(&service.service) + .get_result::(&self.inner.conn) + .map_err(Into::into) + } + + #[cfg(test)] + fn post_user_sync(&self, user: params::PostUser) -> DbResult { + let query = r#" + INSERT INTO users (service, email, generation, client_state, created_at, replaced_at, nodeid, keys_changed_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "#; + diesel::sql_query(query) + .bind::(user.service_id) + .bind::(&user.email) + .bind::(user.generation) + .bind::(&user.client_state) + .bind::(user.created_at) + .bind::, _>(user.replaced_at) + .bind::(user.node_id) + .bind::, _>(user.keys_changed_at) + .execute(&self.inner.conn)?; + + let query = r#" + SELECT uid + FROM users + WHERE email = ? + "#; + diesel::sql_query(query) + .bind::(&user.email) + .get_result::(&self.inner.conn) + .map_err(Into::into) + } +} + +impl Db for TokenserverDb { + sync_db_method!(get_user, get_user_sync, GetUser); + + #[cfg(test)] + sync_db_method!(post_node, post_node_sync, PostNode); + + #[cfg(test)] + sync_db_method!(post_service, post_service_sync, PostService); - Ok(user_record) + #[cfg(test)] + sync_db_method!(post_user, post_user_sync, PostUser); +} + +pub trait Db { + fn get_user(&self, email: String) -> DbFuture<'_, results::GetUser>; + + #[cfg(test)] + fn post_node(&self, node: params::PostNode) -> DbFuture<'_, results::PostNode>; + + #[cfg(test)] + fn post_service(&self, service: params::PostService) -> DbFuture<'_, results::PostService>; + + #[cfg(test)] + fn post_user(&self, user: params::PostUser) -> DbFuture<'_, results::PostUser>; +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::settings::test_settings; + use crate::tokenserver::db::pool::{DbPool, TokenserverPool}; + + type Result = std::result::Result; + + #[tokio::test] + async fn get_user() -> Result<()> { + let pool = db_pool().await?; + let db = pool.get()?; + + // Add a service + let service_id = db.post_service(params::PostService::default()).await?; + + // Add a node + let node_id = { + let node = params::PostNode { + service_id: service_id.id, + ..Default::default() + }; + db.post_node(node).await? + }; + + // Add a user + let email1 = "test_user_1"; + let user_id = { + let user = params::PostUser { + service_id: service_id.id, + node_id: node_id.id, + email: email1.to_owned(), + ..Default::default() + }; + + db.post_user(user).await? + }; + + // Add another user + { + let email2 = "test_user_2"; + let user = params::PostUser { + service_id: service_id.id, + node_id: node_id.id, + email: email2.to_owned(), + ..Default::default() + }; + + db.post_user(user).await?; + } + + let user = db.get_user(email1.to_owned()).await?; + + // Ensure that the correct user has been returned + assert_eq!(user.uid, user_id.uid); + + Ok(()) + } + + pub async fn db_pool() -> DbResult { + let _ = env_logger::try_init(); + + let tokenserver_settings = test_settings().tokenserver; + let use_test_transactions = true; + + TokenserverPool::new(&tokenserver_settings, use_test_transactions) + } } diff --git a/src/tokenserver/db/params.rs b/src/tokenserver/db/params.rs new file mode 100644 index 0000000000..b83d071ac0 --- /dev/null +++ b/src/tokenserver/db/params.rs @@ -0,0 +1,32 @@ +//! Parameter types for database methods. + +pub type GetUser = String; + +#[derive(Default)] +pub struct PostNode { + pub service_id: i32, + pub node: String, + pub available: i32, + pub current_load: i32, + pub capacity: i32, + pub downed: i32, + pub backoff: i32, +} + +#[derive(Default)] +pub struct PostService { + pub service: String, + pub pattern: String, +} + +#[derive(Default)] +pub struct PostUser { + pub service_id: i32, + pub email: String, + pub generation: i64, + pub client_state: String, + pub created_at: i64, + pub replaced_at: Option, + pub node_id: i64, + pub keys_changed_at: Option, +} diff --git a/src/tokenserver/db/pool.rs b/src/tokenserver/db/pool.rs new file mode 100644 index 0000000000..6a40b30b84 --- /dev/null +++ b/src/tokenserver/db/pool.rs @@ -0,0 +1,88 @@ +use diesel::{ + mysql::MysqlConnection, + r2d2::{ConnectionManager, Pool}, + Connection, +}; +#[cfg(test)] +use diesel_logger::LoggingConnection; +use std::time::Duration; + +use super::models::{Db, DbResult, TokenserverDb}; +use crate::db::error::DbError; +use crate::tokenserver::settings::Settings; + +#[cfg(test)] +use crate::db::mysql::TestTransactionCustomizer; + +embed_migrations!("src/tokenserver/migrations"); + +/// Run the diesel embedded migrations +/// +/// Mysql DDL statements implicitly commit which could disrupt MysqlPool's +/// begin_test_transaction during tests. So this runs on its own separate conn. +pub fn run_embedded_migrations(database_url: &str) -> DbResult<()> { + let conn = MysqlConnection::establish(database_url)?; + + #[cfg(test)] + embedded_migrations::run(&LoggingConnection::new(conn))?; + #[cfg(not(test))] + embedded_migrations::run(&conn)?; + + Ok(()) +} + +#[derive(Clone)] +pub struct TokenserverPool { + /// Pool of db connections + inner: Pool>, +} + +impl TokenserverPool { + pub fn new(settings: &Settings, _use_test_transactions: bool) -> DbResult { + run_embedded_migrations(&settings.database_url)?; + + let manager = ConnectionManager::::new(settings.database_url.clone()); + let builder = Pool::builder() + .max_size(settings.database_pool_max_size.unwrap_or(10)) + .connection_timeout(Duration::from_secs( + settings.database_pool_connection_timeout.unwrap_or(30) as u64, + )) + .min_idle(settings.database_pool_min_idle); + + #[cfg(test)] + let builder = if _use_test_transactions { + builder.connection_customizer(Box::new(TestTransactionCustomizer)) + } else { + builder + }; + + Ok(Self { + inner: builder.build(manager)?, + }) + } +} + +impl DbPool for TokenserverPool { + fn get(&self) -> Result, DbError> { + self.inner + .get() + .map(|db_pool| Box::new(TokenserverDb::new(db_pool)) as Box) + .map_err(DbError::from) + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +pub trait DbPool: Sync + Send { + fn get(&self) -> Result, DbError>; + + fn box_clone(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.box_clone() + } +} diff --git a/src/tokenserver/db/results.rs b/src/tokenserver/db/results.rs index 19dc46e963..bb6f15bbf1 100644 --- a/src/tokenserver/db/results.rs +++ b/src/tokenserver/db/results.rs @@ -4,8 +4,11 @@ use diesel::{ }; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, QueryableByName, Serialize)] -pub struct GetTokenserverUser { +#[cfg(test)] +use diesel::sql_types::Integer; + +#[derive(Clone, Debug, Default, Deserialize, QueryableByName, Serialize)] +pub struct GetUser { #[sql_type = "Bigint"] pub uid: i64, #[sql_type = "Text"] @@ -19,3 +22,24 @@ pub struct GetTokenserverUser { #[sql_type = "Bigint"] pub created_at: i64, } + +#[cfg(test)] +#[derive(Default, QueryableByName)] +pub struct PostNode { + #[sql_type = "Bigint"] + pub id: i64, +} + +#[cfg(test)] +#[derive(Default, QueryableByName)] +pub struct PostService { + #[sql_type = "Integer"] + pub id: i32, +} + +#[cfg(test)] +#[derive(Default, QueryableByName)] +pub struct PostUser { + #[sql_type = "Bigint"] + pub uid: i64, +} diff --git a/src/tokenserver/extractors.rs b/src/tokenserver/extractors.rs index a2e5f0168e..0e58737070 100644 --- a/src/tokenserver/extractors.rs +++ b/src/tokenserver/extractors.rs @@ -69,7 +69,10 @@ impl FromRequest for TokenData { .into()); } }; - state.tokenserver_oauth_verifier.verify_token(auth.token()) + // XXX: tokenserver_state will no longer be an Option once the Tokenserver + // code is rolled out, so we will eventually be able to remove this unwrap(). + let tokenserver_state = state.tokenserver_state.as_ref().unwrap(); + tokenserver_state.oauth_verifier.verify_token(auth.token()) }) } } @@ -85,7 +88,9 @@ mod tests { use crate::db::mock::MockDbPool; use crate::server::{metrics, ServerState}; use crate::settings::{Deadman, Secrets, ServerLimits, Settings}; - use crate::tokenserver::MockOAuthVerifier; + use crate::tokenserver::{ + self, db::mock::MockDbPool as MockTokenserverPool, MockOAuthVerifier, + }; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -167,15 +172,19 @@ mod tests { fn make_state(verifier: MockOAuthVerifier) -> ServerState { let settings = Settings::default(); + let tokenserver_state = tokenserver::ServerState { + fxa_email_domain: "test.com".to_owned(), + fxa_metrics_hash_secret: "".to_owned(), + oauth_verifier: Box::new(verifier), + db_pool: Box::new(MockTokenserverPool::new()), + }; ServerState { db_pool: Box::new(MockDbPool::new()), limits: Arc::clone(&SERVER_LIMITS), limits_json: serde_json::to_string(&**SERVER_LIMITS).unwrap(), secrets: Arc::clone(&SECRETS), - tokenserver_database_url: None, - fxa_metrics_hash_secret: None, - tokenserver_oauth_verifier: Box::new(verifier), + tokenserver_state: Some(tokenserver_state), port: 8000, metrics: Box::new(metrics::metrics_from_opts(&settings).unwrap()), quota_enabled: settings.enable_quota, diff --git a/src/tokenserver/handlers.rs b/src/tokenserver/handlers.rs index a20dc2ceaa..005e5fbb59 100644 --- a/src/tokenserver/handlers.rs +++ b/src/tokenserver/handlers.rs @@ -8,7 +8,6 @@ use hmac::{Hmac, Mac, NewMac}; use serde::Serialize; use sha2::Sha256; -use super::db::models::get_tokenserver_user_sync; use super::extractors::TokenserverRequest; use super::support::Tokenlib; use crate::tokenserver::support::MakeTokenPlaintext; @@ -18,7 +17,6 @@ use crate::{ }; const DEFAULT_TOKEN_DURATION: u64 = 5 * 60; -const FXA_EMAIL_DOMAIN: &str = "api.accounts.firefox.com"; #[derive(Debug, Serialize)] pub struct TokenserverResult { @@ -36,19 +34,21 @@ pub async fn get_tokenserver_result( let state = request .app_data::>() .ok_or_else(|| internal_error("Could not load the app state"))?; - let user_email = format!("{}@{}", tokenserver_request.fxa_uid, FXA_EMAIL_DOMAIN); - let tokenserver_user = { - let database_url = state - .tokenserver_database_url - .clone() - .ok_or_else(|| internal_error("Could not load the app state"))?; - get_tokenserver_user_sync(&user_email, &database_url).map_err(ApiError::from)? + let tokenserver_state = state.tokenserver_state.as_ref().unwrap(); + let db = { + let db_pool = tokenserver_state.db_pool.clone(); + db_pool.get().map_err(ApiError::from)? }; - let fxa_metrics_hash_secret = state + let user_email = format!( + "{}@{}", + tokenserver_request.fxa_uid, tokenserver_state.fxa_email_domain + ); + let tokenserver_user = db.get_user(user_email).await?; + + let fxa_metrics_hash_secret = tokenserver_state .fxa_metrics_hash_secret .clone() - .ok_or_else(|| internal_error("Failed to read FxA metrics hash secret"))? .into_bytes(); let hashed_fxa_uid_full = diff --git a/src/tokenserver/migrations/2021-07-16-001122_init/down.sql b/src/tokenserver/migrations/2021-07-16-001122_init/down.sql new file mode 100644 index 0000000000..da49bf74a9 --- /dev/null +++ b/src/tokenserver/migrations/2021-07-16-001122_init/down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS `users`; +DROP TABLE IF EXISTS `nodes`; +DROP TABLE IF EXISTS `services`; diff --git a/src/tokenserver/migrations/2021-07-16-001122_init/up.sql b/src/tokenserver/migrations/2021-07-16-001122_init/up.sql new file mode 100644 index 0000000000..d9e124d46c --- /dev/null +++ b/src/tokenserver/migrations/2021-07-16-001122_init/up.sql @@ -0,0 +1,36 @@ +CREATE TABLE `services` ( + `id` int NOT NULL AUTO_INCREMENT, + `service` varchar(30) DEFAULT NULL, + `pattern` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `service` (`service`) +); + +CREATE TABLE `nodes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `service` int NOT NULL, + `node` varchar(64) NOT NULL, + `available` int NOT NULL DEFAULT '0', + `current_load` int NOT NULL DEFAULT '0', + `capacity` int NOT NULL DEFAULT '0', + `downed` int NOT NULL DEFAULT '0', + `backoff` int NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `service` (`service`), + CONSTRAINT `nodes_ibfk_1` FOREIGN KEY (`service`) REFERENCES `services` (`id`) +); + +CREATE TABLE `users` ( + `uid` bigint NOT NULL AUTO_INCREMENT, + `service` int NOT NULL, + `email` varchar(255) NOT NULL, + `generation` bigint NOT NULL, + `client_state` varchar(32) NOT NULL, + `created_at` bigint NOT NULL, + `replaced_at` bigint DEFAULT NULL, + `nodeid` bigint NOT NULL, + `keys_changed_at` bigint DEFAULT NULL, + PRIMARY KEY (`uid`), + KEY `nodeid` (`nodeid`), + CONSTRAINT `users_ibfk_1` FOREIGN KEY (`nodeid`) REFERENCES `nodes` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 diff --git a/src/tokenserver/mod.rs b/src/tokenserver/mod.rs index bd875d4833..909ad3f88c 100644 --- a/src/tokenserver/mod.rs +++ b/src/tokenserver/mod.rs @@ -1,6 +1,38 @@ pub mod db; pub mod extractors; pub mod handlers; +pub mod settings; pub mod support; pub use self::support::{MockOAuthVerifier, OAuthVerifier, VerifyToken}; + +use db::pool::{DbPool, TokenserverPool}; +use settings::Settings; + +use crate::error::ApiError; + +#[derive(Clone)] +pub struct ServerState { + pub db_pool: Box, + pub fxa_email_domain: String, + pub fxa_metrics_hash_secret: String, + pub oauth_verifier: Box, +} + +impl ServerState { + pub fn from_settings(settings: &Settings) -> Result { + let oauth_verifier = OAuthVerifier { + fxa_oauth_server_url: settings.fxa_oauth_server_url.clone(), + }; + let use_test_transactions = false; + + TokenserverPool::new(settings, use_test_transactions) + .map(|db_pool| ServerState { + fxa_email_domain: settings.fxa_email_domain.clone(), + fxa_metrics_hash_secret: settings.fxa_metrics_hash_secret.clone(), + oauth_verifier: Box::new(oauth_verifier), + db_pool: Box::new(db_pool), + }) + .map_err(Into::into) + } +} diff --git a/src/tokenserver/settings.rs b/src/tokenserver/settings.rs new file mode 100644 index 0000000000..f01744326b --- /dev/null +++ b/src/tokenserver/settings.rs @@ -0,0 +1,43 @@ +use serde::Deserialize; + +#[derive(Clone, Debug, Deserialize)] +pub struct Settings { + pub database_url: String, + + pub database_pool_max_size: Option, + + // NOTE: Not supported by deadpool! + pub database_pool_min_idle: Option, + + /// Pool timeout when waiting for a slot to become available, in seconds + pub database_pool_connection_timeout: Option, + + // XXX: This is a temporary setting used to enable Tokenserver-related features. In + // the future, Tokenserver will always be enabled, and this setting will be + // removed. + pub enabled: bool, + + pub fxa_metrics_hash_secret: String, + + /// The email domain for users' FxA accounts. This should be set according to the + /// desired FxA environment (production or stage). + pub fxa_email_domain: String, + + /// The URL of the FxA server used for verifying Tokenserver OAuth tokens. + pub fxa_oauth_server_url: Option, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + database_url: "mysql://root@127.0.0.1/tokenserver_rs".to_owned(), + database_pool_max_size: None, + database_pool_min_idle: None, + database_pool_connection_timeout: Some(30), + enabled: false, + fxa_email_domain: "api.accounts.firefox.com".to_owned(), + fxa_metrics_hash_secret: "secret".to_owned(), + fxa_oauth_server_url: None, + } + } +} diff --git a/src/tokenserver/support.rs b/src/tokenserver/support.rs index 7ff9a0c5a7..2d5beb5a36 100644 --- a/src/tokenserver/support.rs +++ b/src/tokenserver/support.rs @@ -85,10 +85,18 @@ pub struct TokenData { } /// Implementers of this trait can be used to verify OAuth tokens for Tokenserver. -pub trait VerifyToken { +pub trait VerifyToken: Sync + Send { fn verify_token(&self, token: &str) -> Result; + fn box_clone(&self) -> Box; } +impl Clone for Box { + fn clone(&self) -> Box { + self.box_clone() + } +} + +#[derive(Clone)] /// An adapter to the PyFxA Python library. pub struct OAuthVerifier { pub fxa_oauth_server_url: Option, @@ -134,10 +142,14 @@ impl VerifyToken for OAuthVerifier { .into()), } } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } } /// A mock OAuth verifier to be used for testing purposes. -#[derive(Default)] +#[derive(Clone, Default)] pub struct MockOAuthVerifier { pub valid: bool, pub token_data: TokenData, @@ -155,6 +167,10 @@ impl VerifyToken for MockOAuthVerifier { .into() }) } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } } fn pyerr_to_actix_error(e: PyErr) -> Error { diff --git a/src/web/extractors.rs b/src/web/extractors.rs index e45b6dad4d..0107ad6208 100644 --- a/src/web/extractors.rs +++ b/src/web/extractors.rs @@ -1693,7 +1693,6 @@ mod tests { }; use crate::server::{metrics, ServerState}; use crate::settings::{Deadman, Secrets, ServerLimits, Settings}; - use crate::tokenserver::MockOAuthVerifier; use crate::web::auth::{hkdf_expand_32, HawkPayload}; @@ -1722,9 +1721,7 @@ mod tests { limits: Arc::clone(&SERVER_LIMITS), limits_json: serde_json::to_string(&**SERVER_LIMITS).unwrap(), secrets: Arc::clone(&SECRETS), - tokenserver_database_url: None, - fxa_metrics_hash_secret: None, - tokenserver_oauth_verifier: Box::new(MockOAuthVerifier::default()), + tokenserver_state: None, port: 8000, metrics: Box::new(metrics::metrics_from_opts(&settings).unwrap()), quota_enabled: settings.enable_quota,