From 062cd0ff0176f1f9924e3ae89c0eaf01a8d6fd04 Mon Sep 17 00:00:00 2001 From: pennae Date: Thu, 14 Jul 2022 09:17:16 +0200 Subject: allow integration tests to run in parallel this doesn't do much for performance, but it does allow running the tests with a simple `cargo t`. --- default.nix | 4 --- src/bin/minorskulk.rs | 2 +- src/lib.rs | 9 +++--- tests/api.py | 13 +++++---- tests/conftest.py | 15 ++++++---- tests/integration.rs | 70 ++++++++++++++++++++++++++++++---------------- tests/test_auth_account.py | 4 +-- tests/test_profile.py | 16 +++++------ 8 files changed, 77 insertions(+), 56 deletions(-) diff --git a/default.nix b/default.nix index 1942086..f97d645 100644 --- a/default.nix +++ b/default.nix @@ -32,9 +32,6 @@ rustPlatform.buildRustPackage rec { patchShebangs ./tests/run.sh ''; - # tests can't run multithreaded yet - dontUseCargoParallelTests = true; - # test config for postgres hook and integration tests PGDATABASE = "testdb"; PGUSER = "testuser"; @@ -44,7 +41,6 @@ rustPlatform.buildRustPackage rec { ROCKET_VAPID_KEY = "private_key.pem"; ROCKET_VAPID_SUBJECT = "undefined"; # not needed for tests ROCKET_MAIL_FROM = "minor skulk "; - ROCKET_MAIL_PORT = 2525; preCheck = '' openssl ecparam -genkey -name prime256v1 -out private_key.pem diff --git a/src/bin/minorskulk.rs b/src/bin/minorskulk.rs index 1609edb..21665a9 100644 --- a/src/bin/minorskulk.rs +++ b/src/bin/minorskulk.rs @@ -4,6 +4,6 @@ use minor_skulk::build; async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); - let _ = build().await?.launch().await?; + let _ = build(rocket::build()).await?.launch().await?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 1e6fa31..75232ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ fn default_task_interval() -> std::time::Duration { std::time::Duration::from_secs(5 * 60) } -#[derive(serde::Deserialize)] +#[derive(Debug, serde::Deserialize)] struct Config { database_url: String, location: Absolute<'static>, @@ -203,9 +203,8 @@ async fn ensure_invite_admin(db: &Db, cfg: &Config) -> anyhow::Result<()> { } } -pub async fn build() -> anyhow::Result> { - let rocket = rocket::build(); - let config = rocket.figment().extract::().context("reading config")?; +pub async fn build(base: rocket::Rocket) -> anyhow::Result> { + let config = base.figment().extract::().context("reading config")?; let db = Arc::new(Db::connect(&config.database_url).await.unwrap()); db.migrate().await.context("running db migrations")?; @@ -245,7 +244,7 @@ pub async fn build() -> anyhow::Result> { Ok(()) } }); - let rocket = rocket + let rocket = base .manage(config) .manage(push) .manage(mailer) diff --git a/tests/api.py b/tests/api.py index 9d2f70d..32e1159 100644 --- a/tests/api.py +++ b/tests/api.py @@ -15,12 +15,13 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec from fxa.crypto import quick_stretch_password, derive_key, xor -AUTH_URL = "http://localhost:8000/auth" -PROFILE_URL = "http://localhost:8000/profile" -OAUTH_URL = "http://localhost:8000/oauth" -INVITE_URL = "http://localhost:8000/_invite" -PUSH_PORT = 10264 -SMTP_PORT = 2525 +API_PORT = int(os.environ.get('API_PORT', 8000)) +PUSH_PORT = API_PORT + 1 +SMTP_PORT = int(os.environ.get('MAIL_PORT', 2525)) +AUTH_URL = f"http://localhost:{API_PORT}/auth" +PROFILE_URL = f"http://localhost:{API_PORT}/profile" +OAUTH_URL = f"http://localhost:{API_PORT}/oauth" +INVITE_URL = f"http://localhost:{API_PORT}/_invite" def auth_pw(email, pw): return derive_key(quick_stretch_password(email, pw), "authPW").hex() diff --git a/tests/conftest.py b/tests/conftest.py index 15149cb..43b32f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,27 +52,30 @@ def _account(client, primary, email, mail_server): if e.details['errno'] != 102: raise +email1 = f"test.account-{os.urandom(8).hex()}@test-auth" +email2 = f"test.account2-{os.urandom(8).hex()}@test-auth" + @pytest.fixture(params=[True, False], ids=["primary", "secondary"]) def account(client, request, mail_server): - for a in _account(client, request.param, "test.account@test-auth", mail_server): + for a in _account(client, request.param, email1, mail_server): yield a @pytest.fixture(params=[True, False], ids=["primary", "secondary"]) def account2(client, request, mail_server): - for a in _account(client, request.param, "test.account2@test-auth", mail_server): + for a in _account(client, request.param, email2, mail_server): yield a @pytest.fixture def unverified_account(client, mail_server): - s = client.create_account("test.account@test-auth", "") + s = client.create_account(email1, "") yield s - s.destroy_account("test.account@test-auth", "") + s.destroy_account(s.email, "") @pytest.fixture def login(client, mail_server): - return _login(client, "test.account@test-auth", mail_server) + return _login(client, email1, mail_server) @pytest.fixture def login2(client, mail_server): - return _login(client, "test.account2@test-auth", mail_server) + return _login(client, email2, mail_server) def _refresh_token(account, scope): body = { diff --git a/tests/integration.rs b/tests/integration.rs index afded52..1866702 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -16,31 +16,53 @@ use rocket::{ extern crate rocket; extern crate anyhow; -async fn run_pytest(markers: &'static str, invite: bool) -> anyhow::Result<()> { +async fn run_pytest( + markers: &'static str, + port_offset: u16, + invite_admin: Option<&'static str>, +) -> anyhow::Result<()> { dotenv::dotenv().ok(); // at this point this is only a test runner to be used by the nix build. env::set_var("ROCKET_LOG_LEVEL", "off"); + // allow multiple runs of the server, with different port offsets. + let port = 8000 + port_offset; + let mail_port = 2525 + port_offset; let (tx, rx) = channel(); - let rocket = build().await?.attach(AdHoc::on_liftoff("notify startup", move |rocket| { - Box::pin(async move { - // add an invite code as-if generated during startup and move it to an - // env var, emulating the user looking at the logs and copying the code. - // invite_only needs this to function. - if invite { - let db = rocket.state::().unwrap(); - let tx = db.begin().await.unwrap(); - let mut code = [0; 32]; - OsRng.fill_bytes(&mut code); - let code = base64::encode_config(code, URL_SAFE_NO_PAD); - tx.add_invite_code(&code, Utc::now() + Duration::minutes(5)).await.unwrap(); - tx.commit().await.unwrap(); - env::set_var("INVITE_CODE", code); - } + let rocket = rocket::build(); + let figment = rocket + .figment() + .clone() + .merge((rocket::Config::PORT, port)) + .merge(("location", format!("http://localhost:{}", port))) + .merge(("mail_port", mail_port)); + let figment = if let Some(admin) = invite_admin { + figment.merge(("invite_only", true)).merge(("invite_admin_address", admin)) + } else { + figment.merge(("invite_only", false)) + }; + let rocket = build(rocket.configure(figment)).await?.attach(AdHoc::on_liftoff( + "notify startup", + move |rocket| { + Box::pin(async move { + // add an invite code as-if generated during startup and move it to an + // env var, emulating the user looking at the logs and copying the code. + // invite_only needs this to function. + if invite_admin.is_some() { + let db = rocket.state::().unwrap(); + let tx = db.begin().await.unwrap(); + let mut code = [0; 32]; + OsRng.fill_bytes(&mut code); + let code = base64::encode_config(code, URL_SAFE_NO_PAD); + tx.add_invite_code(&code, Utc::now() + Duration::minutes(5)).await.unwrap(); + tx.commit().await.unwrap(); + env::set_var("INVITE_CODE", code); + } - tx.send(()).unwrap(); - }) - })); + tx.send(()).unwrap(); + }) + }, + )); spawn(async move { rocket.launch().await }); let test = spawn(async move { @@ -49,6 +71,9 @@ async fn run_pytest(markers: &'static str, invite: bool) -> anyhow::Result<()> { .arg("-vvv") .arg("-m") .arg(markers) + .env("API_PORT", port.to_string()) + .env("MAIL_PORT", mail_port.to_string()) + .kill_on_drop(true) .spawn() .expect("failed to spawn"); child.wait().await @@ -61,13 +86,10 @@ async fn run_pytest(markers: &'static str, invite: bool) -> anyhow::Result<()> { #[async_test] async fn open() -> anyhow::Result<()> { - env::set_var("ROCKET_INVITE_ONLY", "false"); - run_pytest("not invite", false).await + run_pytest("not invite", 100, None).await } #[async_test] async fn invite_only() -> anyhow::Result<()> { - env::set_var("ROCKET_INVITE_ONLY", "true"); - env::set_var("ROCKET_INVITE_ADMIN_ADDRESS", "test.account@test-auth"); - run_pytest("invite", true).await + run_pytest("invite", 200, Some("test.account.invite@test-auth")).await } diff --git a/tests/test_auth_account.py b/tests/test_auth_account.py index 68a407b..0e7e22d 100644 --- a/tests/test_auth_account.py +++ b/tests/test_auth_account.py @@ -298,7 +298,7 @@ def test_create_badinvite(client): @pytest.mark.invite def test_create_invite(client, mail_server): # all in one test because restarting the server from python would be a pain - c = client.create_account("test.account@test-auth", "", invite=os.environ['INVITE_CODE']) + c = client.create_account("test.account.invite@test-auth", "", invite=os.environ['INVITE_CODE']) c2 = None try: invite = Invite(c.props['sessionToken']) @@ -319,7 +319,7 @@ def test_create_invite(client, mail_server): assert 'url' in code code = code['url'].split('/')[-1] # code allows registration - c2 = client.create_account("test.account2@test-auth", "", invite=code) + c2 = client.create_account("test.account.invite2@test-auth", "", invite=code) (to, body) = mail_server.wait() data = json.loads(base64.urlsafe_b64decode(body.split("#/verify/", maxsplit=1)[1]).decode('utf8')) c2.post_a('/recovery_email/verify_code', { 'uid': data['uid'], 'code': data['code'] }) diff --git a/tests/test_profile.py b/tests/test_profile.py index 5e7308a..1e21ecc 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -51,7 +51,7 @@ def test_profile(account, profile): resp = profile.get_a("/profile") assert resp == { 'amrValues': None, - 'avatar': 'http://localhost:8000/avatars/00000000000000000000000000000000', + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', 'avatarDefault': True, 'displayName': None, 'email': account.email, @@ -65,7 +65,7 @@ def test_display_name(account, profile): resp = profile.get_a("/profile") assert resp == { 'amrValues': None, - 'avatar': 'http://localhost:8000/avatars/00000000000000000000000000000000', + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', 'avatarDefault': True, 'displayName': None, 'email': account.email, @@ -78,7 +78,7 @@ def test_display_name(account, profile): resp = profile.get_a("/profile") assert resp == { 'amrValues': None, - 'avatar': 'http://localhost:8000/avatars/00000000000000000000000000000000', + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', 'avatarDefault': True, 'displayName': 'foo', 'email': account.email, @@ -91,7 +91,7 @@ def test_display_name(account, profile): def test_avatar(account, profile): resp = profile.get_a("/avatar") assert resp == { - 'avatar': 'http://localhost:8000/avatars/00000000000000000000000000000000', + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', 'avatarDefault': True, 'id': '00000000000000000000000000000000' } @@ -101,11 +101,11 @@ def test_avatar_upload(account, profile): profile.post_a("/avatar/upload", "foo", headers={'content-type': 'image/png'}) resp = profile.get_a("/avatar") new_id = resp['id'] - assert resp['avatar'] != 'http://localhost:8000/avatars/00000000000000000000000000000000' + assert resp['avatar'] != f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000' assert not resp['avatarDefault'] assert resp['id'] != '00000000000000000000000000000000' resp = profile.get_a("/profile") - assert resp['avatar'] != 'http://localhost:8000/avatars/00000000000000000000000000000000' + assert resp['avatar'] != f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000' assert not resp['avatarDefault'] def test_avatar_delete(account, profile): @@ -116,14 +116,14 @@ def test_avatar_delete(account, profile): profile.delete_a(f"/avatar/{new_id}") resp = profile.get_a("/avatar") assert resp == { - 'avatar': 'http://localhost:8000/avatars/00000000000000000000000000000000', + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', 'avatarDefault': True, 'id': '00000000000000000000000000000000' } resp = profile.get_a("/profile") assert resp == { 'amrValues': None, - 'avatar': 'http://localhost:8000/avatars/00000000000000000000000000000000', + 'avatar': f'http://localhost:{API_PORT}/avatars/00000000000000000000000000000000', 'avatarDefault': True, 'displayName': None, 'email': account.email, -- cgit v1.2.3