mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-26 04:04:14 +00:00
Better oauth, appview groundwork
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
mod common;
|
||||
mod helpers;
|
||||
use common::*;
|
||||
use helpers::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Value;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_accounts_as_admin() {
|
||||
let client = client();
|
||||
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
|
||||
let (user_did, _) = setup_new_user("search-target").await;
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&admin_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let accounts = body["accounts"].as_array().expect("accounts should be array");
|
||||
assert!(!accounts.is_empty(), "Should return some accounts");
|
||||
let found = accounts.iter().any(|a| a["did"].as_str() == Some(&user_did));
|
||||
assert!(found, "Should find the created user in results");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_accounts_with_handle_filter() {
|
||||
let client = client();
|
||||
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
|
||||
let ts = chrono::Utc::now().timestamp_millis();
|
||||
let unique_handle = format!("unique-handle-{}.test", ts);
|
||||
let create_payload = serde_json::json!({
|
||||
"handle": unique_handle,
|
||||
"email": format!("unique-{}@searchtest.com", ts),
|
||||
"password": "test-password-123"
|
||||
});
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url().await
|
||||
))
|
||||
.json(&create_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts?handle={}",
|
||||
base_url().await,
|
||||
unique_handle
|
||||
))
|
||||
.bearer_auth(&admin_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let accounts = body["accounts"].as_array().unwrap();
|
||||
assert_eq!(accounts.len(), 1, "Should find exactly one account with this handle");
|
||||
assert_eq!(accounts[0]["handle"].as_str(), Some(unique_handle.as_str()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_accounts_pagination() {
|
||||
let client = client();
|
||||
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
|
||||
for i in 0..3 {
|
||||
let _ = setup_new_user(&format!("search-page-{}", i)).await;
|
||||
}
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts?limit=2",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&admin_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let accounts = body["accounts"].as_array().unwrap();
|
||||
assert_eq!(accounts.len(), 2, "Should return exactly 2 accounts");
|
||||
let cursor = body["cursor"].as_str();
|
||||
assert!(cursor.is_some(), "Should have cursor for more results");
|
||||
let res2 = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts?limit=2&cursor={}",
|
||||
base_url().await,
|
||||
cursor.unwrap()
|
||||
))
|
||||
.bearer_auth(&admin_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res2.status(), StatusCode::OK);
|
||||
let body2: Value = res2.json().await.unwrap();
|
||||
let accounts2 = body2["accounts"].as_array().unwrap();
|
||||
assert!(!accounts2.is_empty(), "Should return more accounts after cursor");
|
||||
let first_page_dids: Vec<&str> = accounts.iter().map(|a| a["did"].as_str().unwrap()).collect();
|
||||
let second_page_dids: Vec<&str> = accounts2.iter().map(|a| a["did"].as_str().unwrap()).collect();
|
||||
for did in &second_page_dids {
|
||||
assert!(!first_page_dids.contains(did), "Second page should not repeat first page DIDs");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_accounts_requires_admin() {
|
||||
let client = client();
|
||||
let (_, user_jwt) = setup_new_user("search-nonadmin").await;
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&user_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_accounts_requires_auth() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts",
|
||||
base_url().await
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_accounts_returns_expected_fields() {
|
||||
let client = client();
|
||||
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
|
||||
let _ = setup_new_user("search-fields").await;
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.admin.searchAccounts?limit=1",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&admin_jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let accounts = body["accounts"].as_array().unwrap();
|
||||
assert!(!accounts.is_empty());
|
||||
let account = &accounts[0];
|
||||
assert!(account["did"].as_str().is_some(), "Should have did");
|
||||
assert!(account["handle"].as_str().is_some(), "Should have handle");
|
||||
assert!(account["indexedAt"].as_str().is_some(), "Should have indexedAt");
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
mod common;
|
||||
mod helpers;
|
||||
use common::*;
|
||||
use helpers::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_password_success() {
|
||||
let client = client();
|
||||
let ts = chrono::Utc::now().timestamp_millis();
|
||||
let handle = format!("change-pw-{}.test", ts);
|
||||
let email = format!("change-pw-{}@test.com", ts);
|
||||
let old_password = "old-password-123";
|
||||
let new_password = "new-password-456";
|
||||
let create_payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": old_password
|
||||
});
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url().await
|
||||
))
|
||||
.json(&create_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
let create_body: Value = create_res.json().await.unwrap();
|
||||
let did = create_body["did"].as_str().unwrap();
|
||||
let jwt = verify_new_account(&client, did).await;
|
||||
let change_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.changePassword",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"currentPassword": old_password,
|
||||
"newPassword": new_password
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to change password");
|
||||
assert_eq!(change_res.status(), StatusCode::OK);
|
||||
let login_old = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createSession",
|
||||
base_url().await
|
||||
))
|
||||
.json(&json!({
|
||||
"identifier": handle,
|
||||
"password": old_password
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to try old password");
|
||||
assert_eq!(login_old.status(), StatusCode::UNAUTHORIZED, "Old password should not work");
|
||||
let login_new = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createSession",
|
||||
base_url().await
|
||||
))
|
||||
.json(&json!({
|
||||
"identifier": handle,
|
||||
"password": new_password
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to try new password");
|
||||
assert_eq!(login_new.status(), StatusCode::OK, "New password should work");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_password_wrong_current() {
|
||||
let client = client();
|
||||
let (_, jwt) = setup_new_user("change-pw-wrong").await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.changePassword",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"currentPassword": "wrong-password",
|
||||
"newPassword": "new-password-123"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
assert_eq!(body["error"].as_str(), Some("InvalidPassword"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_password_too_short() {
|
||||
let client = client();
|
||||
let ts = chrono::Utc::now().timestamp_millis();
|
||||
let handle = format!("change-pw-short-{}.test", ts);
|
||||
let email = format!("change-pw-short-{}@test.com", ts);
|
||||
let password = "correct-password";
|
||||
let create_payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": password
|
||||
});
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url().await
|
||||
))
|
||||
.json(&create_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
let create_body: Value = create_res.json().await.unwrap();
|
||||
let did = create_body["did"].as_str().unwrap();
|
||||
let jwt = verify_new_account(&client, did).await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.changePassword",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"currentPassword": password,
|
||||
"newPassword": "short"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
assert!(body["message"].as_str().unwrap().contains("8 characters"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_password_empty_current() {
|
||||
let client = client();
|
||||
let (_, jwt) = setup_new_user("change-pw-empty").await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.changePassword",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"currentPassword": "",
|
||||
"newPassword": "new-password-123"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_password_empty_new() {
|
||||
let client = client();
|
||||
let (_, jwt) = setup_new_user("change-pw-emptynew").await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.changePassword",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"currentPassword": "e2e-password-123",
|
||||
"newPassword": ""
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_password_requires_auth() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.changePassword",
|
||||
base_url().await
|
||||
))
|
||||
.json(&json!({
|
||||
"currentPassword": "old",
|
||||
"newPassword": "new-password-123"
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
+25
-2
@@ -137,8 +137,12 @@ async fn setup_with_external_infra() -> String {
|
||||
}
|
||||
let mock_server = MockServer::start().await;
|
||||
setup_mock_appview(&mock_server).await;
|
||||
let mock_uri = mock_server.uri();
|
||||
let mock_host = mock_uri.strip_prefix("http://").unwrap_or(&mock_uri);
|
||||
let mock_did = format!("did:web:{}", mock_host.replace(':', "%3A"));
|
||||
setup_mock_did_document(&mock_server, &mock_did, &mock_uri).await;
|
||||
unsafe {
|
||||
std::env::set_var("APPVIEW_URL", mock_server.uri());
|
||||
std::env::set_var("APPVIEW_DID_APP_BSKY", &mock_did);
|
||||
}
|
||||
MOCK_APPVIEW.set(mock_server).ok();
|
||||
spawn_app(database_url).await
|
||||
@@ -186,8 +190,12 @@ async fn setup_with_testcontainers() -> String {
|
||||
let _ = s3_client.create_bucket().bucket("test-bucket").send().await;
|
||||
let mock_server = MockServer::start().await;
|
||||
setup_mock_appview(&mock_server).await;
|
||||
let mock_uri = mock_server.uri();
|
||||
let mock_host = mock_uri.strip_prefix("http://").unwrap_or(&mock_uri);
|
||||
let mock_did = format!("did:web:{}", mock_host.replace(':', "%3A"));
|
||||
setup_mock_did_document(&mock_server, &mock_did, &mock_uri).await;
|
||||
unsafe {
|
||||
std::env::set_var("APPVIEW_URL", mock_server.uri());
|
||||
std::env::set_var("APPVIEW_DID_APP_BSKY", &mock_did);
|
||||
}
|
||||
MOCK_APPVIEW.set(mock_server).ok();
|
||||
S3_CONTAINER.set(s3_container).ok();
|
||||
@@ -215,6 +223,21 @@ async fn setup_with_testcontainers() -> String {
|
||||
);
|
||||
}
|
||||
|
||||
async fn setup_mock_did_document(mock_server: &MockServer, did: &str, service_endpoint: &str) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/.well-known/did.json"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"id": did,
|
||||
"service": [{
|
||||
"id": "#atproto_appview",
|
||||
"type": "AtprotoAppView",
|
||||
"serviceEndpoint": service_endpoint
|
||||
}]
|
||||
})))
|
||||
.mount(mock_server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_mock_appview(mock_server: &MockServer) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/xrpc/app.bsky.actor.getProfile"))
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Value;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_frontend_client_metadata_returns_valid_json() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/oauth/client-metadata.json",
|
||||
base_url().await
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Should return valid JSON");
|
||||
assert!(body["client_id"].as_str().is_some(), "Should have client_id");
|
||||
assert!(body["client_name"].as_str().is_some(), "Should have client_name");
|
||||
assert!(body["redirect_uris"].as_array().is_some(), "Should have redirect_uris");
|
||||
assert!(body["grant_types"].as_array().is_some(), "Should have grant_types");
|
||||
assert!(body["response_types"].as_array().is_some(), "Should have response_types");
|
||||
assert!(body["scope"].as_str().is_some(), "Should have scope");
|
||||
assert!(body["token_endpoint_auth_method"].as_str().is_some(), "Should have token_endpoint_auth_method");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_frontend_client_metadata_correct_values() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/oauth/client-metadata.json",
|
||||
base_url().await
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let client_id = body["client_id"].as_str().unwrap();
|
||||
assert!(client_id.ends_with("/oauth/client-metadata.json"), "client_id should end with /oauth/client-metadata.json");
|
||||
let grant_types = body["grant_types"].as_array().unwrap();
|
||||
let grant_strs: Vec<&str> = grant_types.iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(grant_strs.contains(&"authorization_code"), "Should support authorization_code grant");
|
||||
assert!(grant_strs.contains(&"refresh_token"), "Should support refresh_token grant");
|
||||
let response_types = body["response_types"].as_array().unwrap();
|
||||
let response_strs: Vec<&str> = response_types.iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(response_strs.contains(&"code"), "Should support code response type");
|
||||
assert_eq!(body["token_endpoint_auth_method"].as_str(), Some("none"), "Should be public client (none auth)");
|
||||
assert_eq!(body["application_type"].as_str(), Some("web"), "Should be web application");
|
||||
assert_eq!(body["dpop_bound_access_tokens"].as_bool(), Some(false), "Should not require DPoP");
|
||||
let scope = body["scope"].as_str().unwrap();
|
||||
assert!(scope.contains("atproto"), "Scope should include atproto");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_frontend_client_metadata_redirect_uri_matches_client_uri() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/oauth/client-metadata.json",
|
||||
base_url().await
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let client_uri = body["client_uri"].as_str().unwrap();
|
||||
let redirect_uris = body["redirect_uris"].as_array().unwrap();
|
||||
assert!(!redirect_uris.is_empty(), "Should have at least one redirect URI");
|
||||
let redirect_uri = redirect_uris[0].as_str().unwrap();
|
||||
assert!(redirect_uri.starts_with(client_uri), "Redirect URI should be on same origin as client_uri");
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
mod common;
|
||||
mod helpers;
|
||||
use common::*;
|
||||
use helpers::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_sessions_returns_current_session() {
|
||||
let client = client();
|
||||
let (did, jwt) = setup_new_user("list-sessions").await;
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.bspds.account.listSessions",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let sessions = body["sessions"].as_array().expect("sessions should be array");
|
||||
assert!(!sessions.is_empty(), "Should have at least one session");
|
||||
let current = sessions.iter().find(|s| s["isCurrent"].as_bool() == Some(true));
|
||||
assert!(current.is_some(), "Should have a current session marked");
|
||||
let session = current.unwrap();
|
||||
assert!(session["id"].as_str().is_some(), "Session should have id");
|
||||
assert!(session["createdAt"].as_str().is_some(), "Session should have createdAt");
|
||||
assert!(session["expiresAt"].as_str().is_some(), "Session should have expiresAt");
|
||||
let _ = did;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_sessions_multiple_sessions() {
|
||||
let client = client();
|
||||
let ts = chrono::Utc::now().timestamp_millis();
|
||||
let handle = format!("multi-list-{}.test", ts);
|
||||
let email = format!("multi-list-{}@test.com", ts);
|
||||
let password = "test-password-123";
|
||||
let create_payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": password
|
||||
});
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url().await
|
||||
))
|
||||
.json(&create_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
let create_body: Value = create_res.json().await.unwrap();
|
||||
let did = create_body["did"].as_str().unwrap();
|
||||
let jwt1 = verify_new_account(&client, did).await;
|
||||
let login_payload = json!({
|
||||
"identifier": handle,
|
||||
"password": password
|
||||
});
|
||||
let login_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createSession",
|
||||
base_url().await
|
||||
))
|
||||
.json(&login_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to login");
|
||||
assert_eq!(login_res.status(), StatusCode::OK);
|
||||
let login_body: Value = login_res.json().await.unwrap();
|
||||
let jwt2 = login_body["accessJwt"].as_str().unwrap();
|
||||
let list_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.bspds.account.listSessions",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(jwt2)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list sessions");
|
||||
assert_eq!(list_res.status(), StatusCode::OK);
|
||||
let list_body: Value = list_res.json().await.unwrap();
|
||||
let sessions = list_body["sessions"].as_array().unwrap();
|
||||
assert!(sessions.len() >= 2, "Should have at least 2 sessions, got {}", sessions.len());
|
||||
let _ = jwt1;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_sessions_requires_auth() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.bspds.account.listSessions",
|
||||
base_url().await
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_revoke_session_success() {
|
||||
let client = client();
|
||||
let ts = chrono::Utc::now().timestamp_millis();
|
||||
let handle = format!("revoke-sess-{}.test", ts);
|
||||
let email = format!("revoke-sess-{}@test.com", ts);
|
||||
let password = "test-password-123";
|
||||
let create_payload = json!({
|
||||
"handle": handle,
|
||||
"email": email,
|
||||
"password": password
|
||||
});
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createAccount",
|
||||
base_url().await
|
||||
))
|
||||
.json(&create_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create account");
|
||||
assert_eq!(create_res.status(), StatusCode::OK);
|
||||
let create_body: Value = create_res.json().await.unwrap();
|
||||
let did = create_body["did"].as_str().unwrap();
|
||||
let jwt1 = verify_new_account(&client, did).await;
|
||||
let login_payload = json!({
|
||||
"identifier": handle,
|
||||
"password": password
|
||||
});
|
||||
let login_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.server.createSession",
|
||||
base_url().await
|
||||
))
|
||||
.json(&login_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to login");
|
||||
assert_eq!(login_res.status(), StatusCode::OK);
|
||||
let login_body: Value = login_res.json().await.unwrap();
|
||||
let jwt2 = login_body["accessJwt"].as_str().unwrap();
|
||||
let list_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.bspds.account.listSessions",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(jwt2)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list sessions");
|
||||
let list_body: Value = list_res.json().await.unwrap();
|
||||
let sessions = list_body["sessions"].as_array().unwrap();
|
||||
let other_session = sessions.iter().find(|s| s["isCurrent"].as_bool() != Some(true));
|
||||
assert!(other_session.is_some(), "Should have another session to revoke");
|
||||
let session_id = other_session.unwrap()["id"].as_str().unwrap();
|
||||
let revoke_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.revokeSession",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(jwt2)
|
||||
.json(&json!({"sessionId": session_id}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to revoke session");
|
||||
assert_eq!(revoke_res.status(), StatusCode::OK);
|
||||
let list_after_res = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.bspds.account.listSessions",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(jwt2)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to list sessions after revoke");
|
||||
let list_after_body: Value = list_after_res.json().await.unwrap();
|
||||
let sessions_after = list_after_body["sessions"].as_array().unwrap();
|
||||
let revoked_still_exists = sessions_after.iter().any(|s| s["id"].as_str() == Some(session_id));
|
||||
assert!(!revoked_still_exists, "Revoked session should not appear in list");
|
||||
let _ = jwt1;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_revoke_session_invalid_id() {
|
||||
let client = client();
|
||||
let (_, jwt) = setup_new_user("revoke-invalid").await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.revokeSession",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({"sessionId": "not-a-number"}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_revoke_session_not_found() {
|
||||
let client = client();
|
||||
let (_, jwt) = setup_new_user("revoke-notfound").await;
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.revokeSession",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({"sessionId": "999999999"}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_revoke_session_requires_auth() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.bspds.account.revokeSession",
|
||||
base_url().await
|
||||
))
|
||||
.json(&json!({"sessionId": "1"}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
Reference in New Issue
Block a user