summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpennae <github@quasiparticle.net>2022-07-14 09:17:16 +0200
committerpennae <github@quasiparticle.net>2022-07-14 09:17:16 +0200
commit062cd0ff0176f1f9924e3ae89c0eaf01a8d6fd04 (patch)
treec9d0409c5ec2aa60edad4ee590b746cb645208e7
parent566d6ccee21fc52943cf1862222f2e9a8927403c (diff)
downloadminor-skulk-062cd0ff0176f1f9924e3ae89c0eaf01a8d6fd04.tar.gz
minor-skulk-062cd0ff0176f1f9924e3ae89c0eaf01a8d6fd04.tar.xz
minor-skulk-062cd0ff0176f1f9924e3ae89c0eaf01a8d6fd04.zip
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`.
-rw-r--r--default.nix4
-rw-r--r--src/bin/minorskulk.rs2
-rw-r--r--src/lib.rs9
-rw-r--r--tests/api.py13
-rw-r--r--tests/conftest.py15
-rw-r--r--tests/integration.rs70
-rw-r--r--tests/test_auth_account.py4
-rw-r--r--tests/test_profile.py16
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 <noreply@localhost>";
- 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<rocket::Rocket<rocket::Build>> {
- let rocket = rocket::build();
- let config = rocket.figment().extract::<Config>().context("reading config")?;
+pub async fn build(base: rocket::Rocket<rocket::Build>) -> anyhow::Result<rocket::Rocket<rocket::Build>> {
+ let config = base.figment().extract::<Config>().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<rocket::Rocket<rocket::Build>> {
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::<Db>().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::<Db>().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,