use serde_json::json; use sqlx::{Pool, Postgres}; use windmill_test_utils::*; fn user_url(port: u16, endpoint: &str, name: &str) -> String { format!("http://localhost:{port}/api/w/test-workspace/users/{endpoint}/{name}") } fn client() -> reqwest::Client { reqwest::Client::new() } fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { builder.header("Authorization", "Bearer SECRET_TOKEN") } #[sqlx::test(migrations = "../migrations", fixtures("base"))] async fn test_user_endpoints(db: Pool) -> anyhow::Result<()> { initialize_tracing().await; let server = ApiServer::start(db.clone()).await?; let port = server.addr.port(); let base = format!("http://localhost:{port}/api/w/test-workspace/users"); // ===== Global (non-workspace) endpoints ===== let global_base = format!("http://localhost:{port}/api/users"); // --- global whoami --- let resp = authed(client().get(format!("{global_base}/whoami"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body = resp.json::().await?; assert_eq!(body["email"], "test@windmill.dev"); // --- get_email --- let resp = authed(client().get(format!("{global_base}/email"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let email = resp.text().await?; assert_eq!(email, "test@windmill.dev"); // --- exists_email --- let resp = authed(client().get(format!( "{global_base}/exists/test@windmill.dev" ))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); assert_eq!(resp.json::().await?, true); let resp = authed(client().get(format!( "{global_base}/exists/nonexistent@windmill.dev" ))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); assert_eq!(resp.json::().await?, false); // --- list_as_super_admin --- let resp = authed(client().get(format!("{global_base}/list_as_super_admin"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let list = resp.json::>().await?; assert!(!list.is_empty()); // --- tokens/list --- let resp = authed(client().get(format!("{global_base}/tokens/list"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); resp.json::>().await?; // --- tokens/create --- let resp = authed(client().post(format!("{global_base}/tokens/create"))) .json(&json!({"label": "ephemeral-test-token"})) .send() .await .unwrap(); assert_eq!(resp.status(), 201); let new_token = resp.text().await?; assert!(!new_token.is_empty()); // --- tokens/delete --- let token_prefix = &new_token[..std::cmp::min(new_token.len(), 10)]; let resp = authed(client().delete(format!( "{global_base}/tokens/delete/{token_prefix}" ))) .send() .await .unwrap(); assert_eq!( resp.status(), 200, "delete token: {}", resp.text().await? ); // --- list_invites --- let resp = authed(client().get(format!("{global_base}/list_invites"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); resp.json::>().await?; // --- username_info --- let resp = authed(client().get(format!( "{global_base}/username_info/test@windmill.dev" ))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body = resp.json::().await?; assert_eq!(body["username"], "test-user"); // --- global usage --- let resp = authed(client().get(format!("{global_base}/usage"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); // --- tutorial_progress (get, then set, then get again) --- let resp = authed(client().get(format!("{global_base}/tutorial_progress"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); resp.json::().await?; let resp = authed(client().post(format!("{global_base}/tutorial_progress"))) .json(&json!({"progress": 42, "skipped_all": false})) .send() .await .unwrap(); assert_eq!( resp.status(), 200, "set tutorial_progress: {}", resp.text().await? ); let resp = authed(client().get(format!("{global_base}/tutorial_progress"))) .send() .await .unwrap(); let body = resp.json::().await?; assert_eq!(body["progress"], 42); // --- global update user --- let resp = authed(client().post(format!( "{global_base}/update/test2@windmill.dev" ))) .json(&json!({"name": "Updated Test User 2"})) .send() .await .unwrap(); assert_eq!( resp.status(), 200, "global update user: {}", resp.text().await? ); // --- setpassword (EE-gated in OSS) --- let resp = authed(client().post(format!("{global_base}/setpassword"))) .json(&json!({"password": "new-test-password-123"})) .send() .await .unwrap(); assert!( resp.status() == 200 || resp.status() == 500, "setpassword: unexpected status {}", resp.status() ); // --- all_runnables --- let resp = authed(client().get(format!("{global_base}/all_runnables"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); resp.json::>().await?; // --- onboarding (EE-gated in OSS) --- let resp = authed(client().post(format!("{global_base}/onboarding"))) .json(&json!({"touch_point": "test", "use_case": "testing"})) .send() .await .unwrap(); assert!( resp.status() == 200 || resp.status() == 500, "onboarding: unexpected status {}", resp.status() ); // --- decline_invite (no pending invite, but endpoint should handle gracefully) --- let resp = authed(client().post(format!("{global_base}/decline_invite"))) .json(&json!({"workspace_id": "nonexistent-ws"})) .send() .await .unwrap(); assert!( resp.status() == 200 || resp.status() == 404, "decline_invite: unexpected status {}", resp.status() ); // --- auth: is_first_time_setup (unauthed) --- let resp = client() .get(format!("http://localhost:{port}/api/auth/is_first_time_setup")) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let is_first = resp.json::().await?; assert_eq!(is_first, false); // --- auth: is_smtp_configured (unauthed) --- let resp = client() .get(format!("http://localhost:{port}/api/auth/is_smtp_configured")) .send() .await .unwrap(); assert_eq!(resp.status(), 200); resp.json::().await?; // --- create user (global, EE-gated in OSS) --- let resp = authed(client().post(format!("{global_base}/create"))) .json(&json!({ "email": "newglobaluser@windmill.dev", "password": "test-password-123", "super_admin": false, "name": "New Global User" })) .send() .await .unwrap(); let create_status = resp.status(); assert!( create_status == 201 || create_status == 500, "create user: unexpected status {}", create_status ); if create_status == 201 { // --- rename user (only if create succeeded / EE) --- let resp = authed(client().post(format!( "{global_base}/rename/newglobaluser@windmill.dev" ))) .json(&json!({"new_username": "renamed_user"})) .send() .await .unwrap(); assert_eq!( resp.status(), 200, "rename user: {}", resp.text().await? ); // --- global delete user --- let resp = authed(client().delete(format!( "{global_base}/delete/newglobaluser@windmill.dev" ))) .send() .await .unwrap(); assert_eq!( resp.status(), 200, "global delete user: {}", resp.text().await? ); } // ===== Auth (unauthed) endpoints ===== let auth_base = format!("http://localhost:{port}/api/auth"); // --- login (will fail: password hash in fixture is fake) --- let resp = client() .post(format!("{auth_base}/login")) .json(&json!({"email": "test@windmill.dev", "password": "wrong-password"})) .send() .await .unwrap(); assert!( resp.status() == 400 || resp.status() == 401 || resp.status() == 500, "login: unexpected status {}", resp.status() ); // --- logout (POST, with auth token) --- let resp = authed(client().post(format!("{auth_base}/logout"))) .send() .await .unwrap(); assert!( resp.status() == 200 || resp.status() == 303, "logout POST: unexpected status {}", resp.status() ); // --- logout (GET, with auth token) --- let resp = authed(client().get(format!("{auth_base}/logout"))) .send() .await .unwrap(); assert!( resp.status() == 200 || resp.status() == 303, "logout GET: unexpected status {}", resp.status() ); // --- request_password_reset (returns 400 if SMTP not configured) --- let resp = client() .post(format!("{auth_base}/request_password_reset")) .json(&json!({"email": "test@windmill.dev"})) .send() .await .unwrap(); assert!( resp.status() == 200 || resp.status() == 400, "request_password_reset: unexpected status {}", resp.status() ); // --- reset_password (EE-gated, invalid token) --- let resp = client() .post(format!("{auth_base}/reset_password")) .json(&json!({"token": "invalid-token", "new_password": "new-pass"})) .send() .await .unwrap(); assert!( resp.status() == 400 || resp.status() == 500, "reset_password: unexpected status {}", resp.status() ); // ===== Workspace-scoped endpoints ===== // --- whoami --- let resp = authed(client().get(format!("{base}/whoami"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body = resp.json::().await?; assert_eq!(body["username"], "test-user"); assert_eq!(body["email"], "test@windmill.dev"); assert_eq!(body["is_admin"], true); // --- list --- let resp = authed(client().get(format!("{base}/list"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let list = resp.json::>().await?; assert!(!list.is_empty()); assert!(list.iter().any(|u| u["username"] == "test-user")); // --- list_usernames --- let resp = authed(client().get(format!("{base}/list_usernames"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let usernames = resp.json::>().await?; assert!(usernames.contains(&"test-user".to_string())); // --- get --- let resp = authed(client().get(user_url(port, "get", "test-user"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body = resp.json::().await?; assert_eq!(body["username"], "test-user"); // --- whois --- let resp = authed(client().get(user_url(port, "whois", "test-user"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let body = resp.json::().await?; assert_eq!(body["username"], "test-user"); // --- username_to_email --- let resp = authed(client().get(user_url(port, "username_to_email", "test-user"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); let email = resp.text().await?; assert_eq!(email, "test@windmill.dev"); // --- exists --- let resp = authed(client().post(format!("{base}/exists"))) .json(&json!({"username": "test-user"})) .send() .await .unwrap(); assert_eq!(resp.status(), 200); assert_eq!(resp.json::().await?, true); let resp = authed(client().post(format!("{base}/exists"))) .json(&json!({"username": "nonexistent"})) .send() .await .unwrap(); assert_eq!(resp.status(), 200); assert_eq!(resp.json::().await?, false); // --- list_usage --- let resp = authed(client().get(format!("{base}/list_usage"))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); resp.json::>().await?; // --- is_owner --- let resp = authed(client().get(format!( "http://localhost:{port}/api/w/test-workspace/users/is_owner/u/test-user/test" ))) .send() .await .unwrap(); assert_eq!(resp.status(), 200); // --- update (make non-admin, then revert) --- let resp = authed(client().post(user_url(port, "update", "test-user"))) .json(&json!({"is_admin": false})) .send() .await .unwrap(); assert_eq!(resp.status(), 200, "update user: {}", resp.text().await?); // revert back to admin let resp = authed(client().post(user_url(port, "update", "test-user"))) .json(&json!({"is_admin": true})) .send() .await .unwrap(); assert_eq!(resp.status(), 200); // --- delete workspace user (admin deletes test-user-2) --- let resp = authed(client().delete(user_url(port, "delete", "test-user-2"))) .send() .await .unwrap(); assert_eq!( resp.status(), 200, "delete user: {}", resp.text().await? ); // verify deleted let resp = authed(client().get(format!("{base}/list_usernames"))) .send() .await .unwrap(); let usernames = resp.json::>().await?; assert!(!usernames.contains(&"test-user-2".to_string())); // --- leave workspace (test-user-3 leaves voluntarily) --- let resp = client() .post(format!("{base}/leave")) .header("Authorization", "Bearer SECRET_TOKEN_3") .send() .await .unwrap(); assert_eq!(resp.status(), 200, "leave: {}", resp.text().await?); // verify left let resp = authed(client().get(format!("{base}/list_usernames"))) .send() .await .unwrap(); let usernames = resp.json::>().await?; assert!(!usernames.contains(&"test-user-3".to_string())); Ok(()) }