use cm_api::AppState; use cm_auth::AuthService; use cm_domain::{ AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId, }; use serde_json::{json, Value}; struct TestServer { base: String, client: reqwest::Client, } /// Boots the real axum server on an ephemeral port over real TCP. fn test_runtime(pool: sqlx::PgPool) -> cm_runtime::Runtime { cm_runtime::Runtime::new( pool, std::sync::Arc::new(cm_llm::ScriptedProvider::from_toml("").unwrap()), cm_runtime::RuntimeConfig::basic("scripted", 1024), ) } async fn serve(pool: sqlx::PgPool) -> TestServer { let state = AppState::new(pool.clone(), test_runtime(pool)); let app = cm_api::router(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); TestServer { base: format!("http://{addr}"), client: reqwest::Client::new(), } } async fn seed(pool: &sqlx::PgPool) -> (Workspace, User, Agent) { let ws = Workspace { id: WorkspaceId::new(), name: "Acme".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(pool, &ws).await.unwrap(); let owner = User { id: UserId::new(), workspace_id: ws.id, email: "owner@acme.test".into(), role: Role::Owner, display_name: "Owner".into(), created_at: time::OffsetDateTime::UNIX_EPOCH, }; cm_db::repo::users::insert(pool, &owner).await.unwrap(); let agent = Agent { id: AgentId::new(), workspace_id: ws.id, name: "Scout".into(), job_title: "Research Analyst".into(), system_prompt: String::new(), avatar: "scout-1".into(), accent: "#f96565".into(), wallpaper: "dunes".into(), managed_by: owner.id, status: AgentStatus::Online, }; cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default()) .await .unwrap(); AuthService::new(pool.clone()) .set_password(owner.id, "pw") .await .unwrap(); (ws, owner, agent) } async fn login(server: &TestServer) -> String { let res = server .client .post(format!("{}/api/auth/login", server.base)) .json(&json!({"email": "owner@acme.test", "password": "pw"})) .send() .await .unwrap(); assert_eq!(res.status(), 200); res.json::().await.unwrap()["token"] .as_str() .unwrap() .to_owned() } #[tokio::test] async fn healthz_needs_no_auth() { let pool = cm_testkit::test_pool().await; let server = serve(pool).await; let res = server .client .get(format!("{}/healthz", server.base)) .send() .await .unwrap(); assert_eq!(res.status(), 200); } #[tokio::test] async fn protected_routes_reject_missing_or_bad_tokens() { let pool = cm_testkit::test_pool().await; let server = serve(pool).await; for path in ["/api/user/me", "/api/team/claws", "/api/team/credits"] { let bare = server .client .get(format!("{}{path}", server.base)) .send() .await .unwrap(); assert_eq!(bare.status(), 401, "{path} without token"); let bad = server .client .get(format!("{}{path}", server.base)) .bearer_auth("forged-token") .send() .await .unwrap(); assert_eq!(bad.status(), 401, "{path} with forged token"); } } #[tokio::test] async fn login_rejects_wrong_password() { let pool = cm_testkit::test_pool().await; seed(&pool).await; let server = serve(pool).await; let res = server .client .post(format!("{}/api/auth/login", server.base)) .json(&json!({"email": "owner@acme.test", "password": "nope"})) .send() .await .unwrap(); assert_eq!(res.status(), 401); } #[tokio::test] async fn user_me_returns_the_authenticated_user() { let pool = cm_testkit::test_pool().await; let (ws, owner, _) = seed(&pool).await; let server = serve(pool).await; let token = login(&server).await; let me: Value = server .client .get(format!("{}/api/user/me", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(me["id"], owner.id.to_string()); assert_eq!(me["workspace_id"], ws.id.to_string()); assert_eq!(me["email"], "owner@acme.test"); assert_eq!(me["role"], "owner"); } #[tokio::test] async fn team_claws_lists_the_roster() { let pool = cm_testkit::test_pool().await; let (_, _, agent) = seed(&pool).await; let server = serve(pool).await; let token = login(&server).await; let claws: Value = server .client .get(format!("{}/api/team/claws", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); let list = claws.as_array().unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0]["id"], agent.id.to_string()); assert_eq!(list[0]["name"], "Scout"); assert_eq!(list[0]["status"], "online"); } #[tokio::test] async fn team_credits_returns_available_balance() { let pool = cm_testkit::test_pool().await; let (ws, _, _) = seed(&pool).await; cm_db::repo::credits::add_lot(&pool, ws.id, 500, "purchase") .await .unwrap(); let server = serve(pool).await; let token = login(&server).await; let credits: Value = server .client .get(format!("{}/api/team/credits", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(credits["available"], 500); } #[tokio::test] async fn team_permissions_reflect_role() { let pool = cm_testkit::test_pool().await; seed(&pool).await; let server = serve(pool).await; let token = login(&server).await; let perms: Value = server .client .get(format!("{}/api/team/permissions", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); assert_eq!(perms["role"], "owner"); assert_eq!(perms["can_manage_team"], true); assert_eq!(perms["can_manage_billing"], true); } #[tokio::test] async fn logout_revokes_the_session() { let pool = cm_testkit::test_pool().await; seed(&pool).await; let server = serve(pool).await; let token = login(&server).await; let res = server .client .post(format!("{}/api/auth/logout", server.base)) .bearer_auth(&token) .send() .await .unwrap(); assert_eq!(res.status(), 204); let me = server .client .get(format!("{}/api/user/me", server.base)) .bearer_auth(&token) .send() .await .unwrap(); assert_eq!(me.status(), 401); } #[tokio::test] async fn team_members_lists_workspace_users() { let pool = cm_testkit::test_pool().await; let (_, owner, _) = seed(&pool).await; let server = serve(pool).await; let token = login(&server).await; let members: Value = server .client .get(format!("{}/api/team/members", server.base)) .bearer_auth(&token) .send() .await .unwrap() .json() .await .unwrap(); let list = members.as_array().unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0]["id"], owner.id.to_string()); assert_eq!(list[0]["role"], "owner"); assert_eq!(list[0]["display_name"], "Owner"); }