mod common; use common::*; use chrono::Utc; use reqwest::{StatusCode, header}; use serde_json::{Value, json}; #[tokio::test] async fn test_get_record_not_found() { let client = client(); let (_, did) = create_account_and_login(&client).await; let params = [ ("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "nonexistent"), ]; let res = client .get(format!( "{}/xrpc/com.atproto.repo.getRecord", base_url().await )) .query(¶ms) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_upload_blob_no_auth() { let client = client(); let res = client .post(format!( "{}/xrpc/com.atproto.repo.uploadBlob", base_url().await )) .header(header::CONTENT_TYPE, "text/plain") .body("no auth") .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); let body: Value = res.json().await.expect("Response was not valid JSON"); assert_eq!(body["error"], "AuthenticationRequired"); } #[tokio::test] async fn test_upload_blob_success() { let client = client(); let (token, _) = create_account_and_login(&client).await; let res = client .post(format!( "{}/xrpc/com.atproto.repo.uploadBlob", base_url().await )) .header(header::CONTENT_TYPE, "text/plain") .bearer_auth(token) .body("This is our blob data") .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); assert!(body["blob"]["ref"]["$link"].as_str().is_some()); } #[tokio::test] async fn test_put_record_no_auth() { let client = client(); let payload = json!({ "repo": "did:plc:123", "collection": "app.bsky.feed.post", "rkey": "fake", "record": {} }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .json(&payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); let body: Value = res.json().await.expect("Response was not valid JSON"); assert_eq!(body["error"], "AuthenticationRequired"); } #[tokio::test] async fn test_put_record_success() { let client = client(); let (token, did) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": "e2e_test_post", "record": { "$type": "app.bsky.feed.post", "text": "Hello from the e2e test script!", "createdAt": now } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .bearer_auth(token) .json(&payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); assert!(body.get("uri").is_some()); assert!(body.get("cid").is_some()); } #[tokio::test] async fn test_get_record_missing_params() { let client = client(); let params = [("repo", "did:plc:12345")]; let res = client .get(format!( "{}/xrpc/com.atproto.repo.getRecord", base_url().await )) .query(¶ms) .send() .await .expect("Failed to send request"); assert_eq!( res.status(), StatusCode::BAD_REQUEST, "Expected 400 for missing params" ); } #[tokio::test] async fn test_upload_blob_bad_token() { let client = client(); let res = client .post(format!( "{}/xrpc/com.atproto.repo.uploadBlob", base_url().await )) .header(header::CONTENT_TYPE, "text/plain") .bearer_auth(BAD_AUTH_TOKEN) .body("This is our blob data") .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); let body: Value = res.json().await.expect("Response was not valid JSON"); assert_eq!(body["error"], "AuthenticationFailed"); } #[tokio::test] async fn test_put_record_mismatched_repo() { let client = client(); let (token, _) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let payload = json!({ "repo": "did:plc:OTHER-USER", "collection": "app.bsky.feed.post", "rkey": "e2e_test_post", "record": { "$type": "app.bsky.feed.post", "text": "Hello from the e2e test script!", "createdAt": now } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .bearer_auth(token) .json(&payload) .send() .await .expect("Failed to send request"); assert!( res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, "Expected 403 or 401 for mismatched repo and auth, got {}", res.status() ); } #[tokio::test] async fn test_put_record_invalid_schema() { let client = client(); let (token, did) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": "e2e_test_invalid", "record": { "$type": "app.bsky.feed.post", "createdAt": now } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .bearer_auth(token) .json(&payload) .send() .await .expect("Failed to send request"); assert_eq!( res.status(), StatusCode::BAD_REQUEST, "Expected 400 for invalid record schema" ); } #[tokio::test] async fn test_upload_blob_unsupported_mime_type() { let client = client(); let (token, _) = create_account_and_login(&client).await; let res = client .post(format!( "{}/xrpc/com.atproto.repo.uploadBlob", base_url().await )) .header(header::CONTENT_TYPE, "application/xml") .bearer_auth(token) .body("not an image") .send() .await .expect("Failed to send request"); // Changed expectation to OK for now, bc we don't validate mime type strictly yet. assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn test_list_records() { let client = client(); let (_, did) = create_account_and_login(&client).await; let params = [ ("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "10"), ]; let res = client .get(format!( "{}/xrpc/com.atproto.repo.listRecords", base_url().await )) .query(¶ms) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn test_describe_repo() { let client = client(); let (_, did) = create_account_and_login(&client).await; let params = [("repo", did.as_str())]; let res = client .get(format!( "{}/xrpc/com.atproto.repo.describeRepo", base_url().await )) .query(¶ms) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); } #[tokio::test] async fn test_create_record_success_with_generated_rkey() { let client = client(); let (token, did) = create_account_and_login(&client).await; let payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "record": { "$type": "app.bsky.feed.post", "text": "Hello, world!", "createdAt": "2025-12-02T12:00:00Z" } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.createRecord", base_url().await )) .json(&payload) .bearer_auth(token) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); let uri = body["uri"].as_str().unwrap(); assert!(uri.starts_with(&format!("at://{}/app.bsky.feed.post/", did))); assert!(body.get("cid").is_some()); } #[tokio::test] async fn test_create_record_success_with_provided_rkey() { let client = client(); let (token, did) = create_account_and_login(&client).await; let rkey = format!("custom-rkey-{}", Utc::now().timestamp_millis()); let payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, "record": { "$type": "app.bsky.feed.post", "text": "Hello, world!", "createdAt": "2025-12-02T12:00:00Z" } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.createRecord", base_url().await )) .json(&payload) .bearer_auth(token) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); assert_eq!( body["uri"], format!("at://{}/app.bsky.feed.post/{}", did, rkey) ); assert!(body.get("cid").is_some()); } #[tokio::test] async fn test_delete_record() { let client = client(); let (token, did) = create_account_and_login(&client).await; let rkey = format!("post_to_delete_{}", Utc::now().timestamp_millis()); let create_payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, "record": { "$type": "app.bsky.feed.post", "text": "This post will be deleted", "createdAt": Utc::now().to_rfc3339() } }); let create_res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .bearer_auth(&token) .json(&create_payload) .send() .await .expect("Failed to create record"); assert_eq!(create_res.status(), StatusCode::OK); let delete_payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey }); let delete_res = client .post(format!( "{}/xrpc/com.atproto.repo.deleteRecord", base_url().await )) .bearer_auth(&token) .json(&delete_payload) .send() .await .expect("Failed to send request"); assert_eq!(delete_res.status(), StatusCode::OK); let get_res = client .get(format!( "{}/xrpc/com.atproto.repo.getRecord", base_url().await )) .query(&[ ("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", rkey.as_str()), ]) .send() .await .expect("Failed to verify deletion"); assert_eq!(get_res.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_apply_writes_create() { let client = client(); let (token, did) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let payload = json!({ "repo": did, "writes": [ { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "value": { "$type": "app.bsky.feed.post", "text": "Batch created post 1", "createdAt": now } }, { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "value": { "$type": "app.bsky.feed.post", "text": "Batch created post 2", "createdAt": now } } ] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .bearer_auth(&token) .json(&payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); assert!(body["commit"]["cid"].is_string()); assert!(body["results"].is_array()); let results = body["results"].as_array().unwrap(); assert_eq!(results.len(), 2); assert!(results[0]["uri"].is_string()); assert!(results[0]["cid"].is_string()); } #[tokio::test] async fn test_apply_writes_update() { let client = client(); let (token, did) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let rkey = format!("batch_update_{}", Utc::now().timestamp_millis()); let create_payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, "record": { "$type": "app.bsky.feed.post", "text": "Original post", "createdAt": now } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .bearer_auth(&token) .json(&create_payload) .send() .await .expect("Failed to create"); assert_eq!(res.status(), StatusCode::OK); let update_payload = json!({ "repo": did, "writes": [ { "$type": "com.atproto.repo.applyWrites#update", "collection": "app.bsky.feed.post", "rkey": rkey, "value": { "$type": "app.bsky.feed.post", "text": "Updated post via applyWrites", "createdAt": now } } ] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .bearer_auth(&token) .json(&update_payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); let results = body["results"].as_array().unwrap(); assert_eq!(results.len(), 1); assert!(results[0]["uri"].is_string()); } #[tokio::test] async fn test_apply_writes_delete() { let client = client(); let (token, did) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let rkey = format!("batch_delete_{}", Utc::now().timestamp_millis()); let create_payload = json!({ "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, "record": { "$type": "app.bsky.feed.post", "text": "Post to delete", "createdAt": now } }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.putRecord", base_url().await )) .bearer_auth(&token) .json(&create_payload) .send() .await .expect("Failed to create"); assert_eq!(res.status(), StatusCode::OK); let delete_payload = json!({ "repo": did, "writes": [ { "$type": "com.atproto.repo.applyWrites#delete", "collection": "app.bsky.feed.post", "rkey": rkey } ] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .bearer_auth(&token) .json(&delete_payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let get_res = client .get(format!( "{}/xrpc/com.atproto.repo.getRecord", base_url().await )) .query(&[ ("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", rkey.as_str()), ]) .send() .await .expect("Failed to verify"); assert_eq!(get_res.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_apply_writes_mixed_operations() { let client = client(); let (token, did) = create_account_and_login(&client).await; let now = Utc::now().to_rfc3339(); let rkey_to_delete = format!("mixed_del_{}", Utc::now().timestamp_millis()); let rkey_to_update = format!("mixed_upd_{}", Utc::now().timestamp_millis()); let setup_payload = json!({ "repo": did, "writes": [ { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": rkey_to_delete, "value": { "$type": "app.bsky.feed.post", "text": "To be deleted", "createdAt": now } }, { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": rkey_to_update, "value": { "$type": "app.bsky.feed.post", "text": "To be updated", "createdAt": now } } ] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .bearer_auth(&token) .json(&setup_payload) .send() .await .expect("Failed to setup"); assert_eq!(res.status(), StatusCode::OK); let mixed_payload = json!({ "repo": did, "writes": [ { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "value": { "$type": "app.bsky.feed.post", "text": "New post", "createdAt": now } }, { "$type": "com.atproto.repo.applyWrites#update", "collection": "app.bsky.feed.post", "rkey": rkey_to_update, "value": { "$type": "app.bsky.feed.post", "text": "Updated text", "createdAt": now } }, { "$type": "com.atproto.repo.applyWrites#delete", "collection": "app.bsky.feed.post", "rkey": rkey_to_delete } ] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .bearer_auth(&token) .json(&mixed_payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); let results = body["results"].as_array().unwrap(); assert_eq!(results.len(), 3); } #[tokio::test] async fn test_apply_writes_no_auth() { let client = client(); let payload = json!({ "repo": "did:plc:test", "writes": [ { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "value": { "$type": "app.bsky.feed.post", "text": "Test", "createdAt": "2025-01-01T00:00:00Z" } } ] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .json(&payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_apply_writes_empty_writes() { let client = client(); let (token, did) = create_account_and_login(&client).await; let payload = json!({ "repo": did, "writes": [] }); let res = client .post(format!( "{}/xrpc/com.atproto.repo.applyWrites", base_url().await )) .bearer_auth(&token) .json(&payload) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_list_missing_blobs() { let client = client(); let (access_jwt, _) = create_account_and_login(&client).await; let res = client .get(format!( "{}/xrpc/com.atproto.repo.listMissingBlobs", base_url().await )) .bearer_auth(&access_jwt) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.expect("Response was not valid JSON"); assert!(body["blobs"].is_array()); } #[tokio::test] async fn test_list_missing_blobs_no_auth() { let client = client(); let res = client .get(format!( "{}/xrpc/com.atproto.repo.listMissingBlobs", base_url().await )) .send() .await .expect("Failed to send request"); assert_eq!(res.status(), StatusCode::UNAUTHORIZED); }