Rest of lifecycle tests split out to other files

This commit is contained in:
Lewis
2025-12-09 19:54:05 +02:00
parent 04a70e590c
commit 7559378fa8
20 changed files with 895 additions and 2415 deletions

View File

@@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT code, available_uses, created_at, disabled\n FROM invite_codes\n WHERE created_by_user = $1\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "code",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "available_uses",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "disabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET invites_disabled = TRUE WHERE did = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "3609b5817e4564b824b0c0f4fe32488ee7caed02cee08fb163e4914c5349eb11"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "411a7cff2d43612379903d6343da0761ae5b8b30a2fa1c89afb85047d4fbe3eb"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "413c5b03501a399dca13f345fcae05770517091d73db93966853e944c68ee237"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "41d35cebdf29be500e30ef636ad96450620f71087c174e5a74446fcdb29a2ba8"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT u.did, icu.used_at\n FROM invite_code_uses icu\n JOIN users u ON icu.used_by_user = u.id\n WHERE icu.code = $1\n ORDER BY icu.used_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "used_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "5d5442136932d4088873a935c41cb3a683c4771e4fb8c151b3fd5119fb6c1068"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT cid, takedown_ref FROM blobs WHERE cid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "cid",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "takedown_ref",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "62942bd21d545eb15bfea4f46378b6c2ebfe12b8bc9e27c63a6c0f77a9105303"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did FROM users WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "6819c68a3c06083a826eb94271cc8ff0d4c2bbd33b9051f50a1a46ecc8d3e85b"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET invites_disabled = FALSE WHERE did = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "78ed180c33b8f1f7a3adcd3dd0e7e5988ae1dbc2e10009df9fe44fb0fbbe95b3"
}

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE invite_codes SET disabled = TRUE WHERE code = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "7b2d1d4ac06063e07a7c7a7d0fb434db08ce312eb2864405d7f96f4e985ed036"
}

View File

@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "deactivated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "takedown_ref",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true
]
},
"hash": "7d1617283733986244b8129cdd14ec1d04510aa73e4ae350a54f57629b9eaff9"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Uuid"
]
},
"nullable": []
},
"hash": "bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET takedown_ref = $1 WHERE did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "cd25ddc034a51748f699e2fcd1312691123aee9904eb2ee4073ed0f2c8c49bf9"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT invites_disabled FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "invites_disabled",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c"
}

View File

@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "takedown_ref",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "fbc8ab04fe5e06d6e6de9a4eeaabee8af9ee887812bcfe5893df1c7e682747c1"
}

File diff suppressed because it is too large Load Diff

View File

@@ -304,3 +304,142 @@ async fn test_app_password_lifecycle() {
let passwords_after = list_after["passwords"].as_array().unwrap();
assert_eq!(passwords_after.len(), 0, "No app passwords should remain");
}
#[tokio::test]
async fn test_account_deactivation_lifecycle() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("deactivate-{}.test", ts);
let email = format!("deactivate-{}@test.com", ts);
let password = "deactivate-password";
let create_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&json!({
"handle": handle,
"email": email,
"password": password
}))
.send()
.await
.expect("Failed to create account");
assert_eq!(create_res.status(), StatusCode::OK);
let account: Value = create_res.json().await.unwrap();
let did = account["did"].as_str().unwrap().to_string();
let jwt = account["accessJwt"].as_str().unwrap().to_string();
let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
let post_rkey = post_uri.split('/').last().unwrap();
let status_before = client
.get(format!(
"{}/xrpc/com.atproto.server.checkAccountStatus",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to check status");
assert_eq!(status_before.status(), StatusCode::OK);
let status_body: Value = status_before.json().await.unwrap();
assert_eq!(status_body["activated"], true);
let deactivate_res = client
.post(format!(
"{}/xrpc/com.atproto.server.deactivateAccount",
base_url().await
))
.bearer_auth(&jwt)
.json(&json!({}))
.send()
.await
.expect("Failed to deactivate");
assert_eq!(deactivate_res.status(), StatusCode::OK);
let get_post_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&[
("repo", did.as_str()),
("collection", "app.bsky.feed.post"),
("rkey", post_rkey),
])
.send()
.await
.expect("Failed to get post while deactivated");
assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable");
let activate_res = client
.post(format!(
"{}/xrpc/com.atproto.server.activateAccount",
base_url().await
))
.bearer_auth(&jwt)
.json(&json!({}))
.send()
.await
.expect("Failed to reactivate");
assert_eq!(activate_res.status(), StatusCode::OK);
let status_after_activate = client
.get(format!(
"{}/xrpc/com.atproto.server.checkAccountStatus",
base_url().await
))
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to check status after activate");
assert_eq!(status_after_activate.status(), StatusCode::OK);
let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await;
assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation");
}
#[tokio::test]
async fn test_service_auth_lifecycle() {
let client = client();
let (did, jwt) = setup_new_user("service-auth-test").await;
let service_auth_res = client
.get(format!(
"{}/xrpc/com.atproto.server.getServiceAuth",
base_url().await
))
.query(&[
("aud", "did:web:api.bsky.app"),
("lxm", "com.atproto.repo.uploadBlob"),
])
.bearer_auth(&jwt)
.send()
.await
.expect("Failed to get service auth");
assert_eq!(service_auth_res.status(), StatusCode::OK);
let auth_body: Value = service_auth_res.json().await.unwrap();
let service_token = auth_body["token"].as_str().expect("No token in response");
let parts: Vec<&str> = service_token.split('.').collect();
assert_eq!(parts.len(), 3, "Service token should be a valid JWT");
use base64::Engine;
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(parts[1])
.expect("Failed to decode JWT payload");
let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload");
assert_eq!(claims["iss"], did);
assert_eq!(claims["aud"], "did:web:api.bsky.app");
assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob");
}

View File

@@ -7,6 +7,7 @@ use helpers::*;
use reqwest::StatusCode;
use serde_json::{Value, json};
use std::time::Duration;
use chrono::Utc;
#[tokio::test]
async fn test_social_flow_lifecycle() {
@@ -414,3 +415,102 @@ async fn test_mutual_follow_lifecycle() {
let bob_feed = bob_tl["feed"].as_array().unwrap();
assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post");
}
#[tokio::test]
async fn test_account_to_post_full_lifecycle() {
let client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("fullcycle-{}.test", ts);
let email = format!("fullcycle-{}@test.com", ts);
let password = "fullcycle-password";
let create_account_res = client
.post(format!(
"{}/xrpc/com.atproto.server.createAccount",
base_url().await
))
.json(&json!({
"handle": handle,
"email": email,
"password": password
}))
.send()
.await
.expect("Failed to create account");
assert_eq!(create_account_res.status(), StatusCode::OK);
let account_body: Value = create_account_res.json().await.unwrap();
let did = account_body["did"].as_str().unwrap().to_string();
let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string();
let get_session_res = client
.get(format!(
"{}/xrpc/com.atproto.server.getSession",
base_url().await
))
.bearer_auth(&access_jwt)
.send()
.await
.expect("Failed to get session");
assert_eq!(get_session_res.status(), StatusCode::OK);
let session_body: Value = get_session_res.json().await.unwrap();
assert_eq!(session_body["did"], did);
assert_eq!(session_body["handle"], handle);
let profile_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&access_jwt)
.json(&json!({
"repo": did,
"collection": "app.bsky.actor.profile",
"rkey": "self",
"record": {
"$type": "app.bsky.actor.profile",
"displayName": "Full Cycle User"
}
}))
.send()
.await
.expect("Failed to create profile");
assert_eq!(profile_res.status(), StatusCode::OK);
let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await;
let get_post_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.getRecord",
base_url().await
))
.query(&[
("repo", did.as_str()),
("collection", "app.bsky.feed.post"),
("rkey", post_uri.split('/').last().unwrap()),
])
.send()
.await
.expect("Failed to get post");
assert_eq!(get_post_res.status(), StatusCode::OK);
create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await;
let describe_res = client
.get(format!(
"{}/xrpc/com.atproto.repo.describeRepo",
base_url().await
))
.query(&[("repo", did.as_str())])
.send()
.await
.expect("Failed to describe repo");
assert_eq!(describe_res.status(), StatusCode::OK);
let describe_body: Value = describe_res.json().await.unwrap();
assert_eq!(describe_body["did"], did);
assert_eq!(describe_body["handle"], handle);
}

67
tests/moderation.rs Normal file
View File

@@ -0,0 +1,67 @@
mod common;
mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode;
use serde_json::{Value, json};
#[tokio::test]
async fn test_moderation_report_lifecycle() {
let client = client();
let (alice_did, alice_jwt) = setup_new_user("alice-report").await;
let (bob_did, bob_jwt) = setup_new_user("bob-report").await;
let (post_uri, post_cid) =
create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await;
let report_payload = json!({
"reasonType": "com.atproto.moderation.defs#reasonSpam",
"reason": "This looks like spam to me",
"subject": {
"$type": "com.atproto.repo.strongRef",
"uri": post_uri,
"cid": post_cid
}
});
let report_res = client
.post(format!(
"{}/xrpc/com.atproto.moderation.createReport",
base_url().await
))
.bearer_auth(&alice_jwt)
.json(&report_payload)
.send()
.await
.expect("Failed to create report");
assert_eq!(report_res.status(), StatusCode::OK);
let report_body: Value = report_res.json().await.unwrap();
assert!(report_body["id"].is_number(), "Report should have an ID");
assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam");
assert_eq!(report_body["reportedBy"], alice_did);
let account_report_payload = json!({
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reason": "Suspicious account activity",
"subject": {
"$type": "com.atproto.admin.defs#repoRef",
"did": bob_did
}
});
let account_report_res = client
.post(format!(
"{}/xrpc/com.atproto.moderation.createReport",
base_url().await
))
.bearer_auth(&alice_jwt)
.json(&account_report_payload)
.send()
.await
.expect("Failed to create account report");
assert_eq!(account_report_res.status(), StatusCode::OK);
}

View File

@@ -1,7 +1,12 @@
mod common;
mod helpers;
use common::*;
use helpers::*;
use reqwest::StatusCode;
use serde_json::Value;
use reqwest::header;
use serde_json::{Value, json};
use chrono::Utc;
#[tokio::test]
async fn test_get_latest_commit_success() {
@@ -429,3 +434,267 @@ async fn test_get_blocks_not_found() {
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_sync_record_lifecycle() {
let client = client();
let (did, jwt) = setup_new_user("sync-record-lifecycle").await;
let (post_uri, _post_cid) =
create_post(&client, &did, &jwt, "Post for sync record test").await;
let post_rkey = post_uri.split('/').last().unwrap();
let sync_record_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord",
base_url().await
))
.query(&[
("did", did.as_str()),
("collection", "app.bsky.feed.post"),
("rkey", post_rkey),
])
.send()
.await
.expect("Failed to get sync record");
assert_eq!(sync_record_res.status(), StatusCode::OK);
assert_eq!(
sync_record_res
.headers()
.get("content-type")
.and_then(|h| h.to_str().ok()),
Some("application/vnd.ipld.car")
);
let car_bytes = sync_record_res.bytes().await.unwrap();
assert!(!car_bytes.is_empty(), "CAR data should not be empty");
let latest_before = client
.get(format!(
"{}/xrpc/com.atproto.sync.getLatestCommit",
base_url().await
))
.query(&[("did", did.as_str())])
.send()
.await
.expect("Failed to get latest commit");
let latest_before_body: Value = latest_before.json().await.unwrap();
let rev_before = latest_before_body["rev"].as_str().unwrap().to_string();
let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await;
let latest_after = client
.get(format!(
"{}/xrpc/com.atproto.sync.getLatestCommit",
base_url().await
))
.query(&[("did", did.as_str())])
.send()
.await
.expect("Failed to get latest commit after");
let latest_after_body: Value = latest_after.json().await.unwrap();
let rev_after = latest_after_body["rev"].as_str().unwrap().to_string();
assert_ne!(rev_before, rev_after, "Revision should change after new record");
let delete_payload = json!({
"repo": did,
"collection": "app.bsky.feed.post",
"rkey": post_rkey
});
let delete_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.deleteRecord",
base_url().await
))
.bearer_auth(&jwt)
.json(&delete_payload)
.send()
.await
.expect("Failed to delete record");
assert_eq!(delete_res.status(), StatusCode::OK);
let sync_deleted_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord",
base_url().await
))
.query(&[
("did", did.as_str()),
("collection", "app.bsky.feed.post"),
("rkey", post_rkey),
])
.send()
.await
.expect("Failed to check deleted record via sync");
assert_eq!(
sync_deleted_res.status(),
StatusCode::NOT_FOUND,
"Deleted record should return 404 via sync.getRecord"
);
let post2_rkey = post2_uri.split('/').last().unwrap();
let sync_post2_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getRecord",
base_url().await
))
.query(&[
("did", did.as_str()),
("collection", "app.bsky.feed.post"),
("rkey", post2_rkey),
])
.send()
.await
.expect("Failed to get second post via sync");
assert_eq!(
sync_post2_res.status(),
StatusCode::OK,
"Second post should still be accessible"
);
}
#[tokio::test]
async fn test_sync_repo_export_lifecycle() {
let client = client();
let (did, jwt) = setup_new_user("sync-repo-export").await;
let profile_payload = json!({
"repo": did,
"collection": "app.bsky.actor.profile",
"rkey": "self",
"record": {
"$type": "app.bsky.actor.profile",
"displayName": "Sync Export User"
}
});
let profile_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.putRecord",
base_url().await
))
.bearer_auth(&jwt)
.json(&profile_payload)
.send()
.await
.expect("Failed to create profile");
assert_eq!(profile_res.status(), StatusCode::OK);
for i in 0..3 {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await;
}
let blob_data = b"blob data for sync export test";
let upload_res = client
.post(format!(
"{}/xrpc/com.atproto.repo.uploadBlob",
base_url().await
))
.header(header::CONTENT_TYPE, "application/octet-stream")
.bearer_auth(&jwt)
.body(blob_data.to_vec())
.send()
.await
.expect("Failed to upload blob");
assert_eq!(upload_res.status(), StatusCode::OK);
let blob_body: Value = upload_res.json().await.unwrap();
let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string();
let repo_status_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getRepoStatus",
base_url().await
))
.query(&[("did", did.as_str())])
.send()
.await
.expect("Failed to get repo status");
assert_eq!(repo_status_res.status(), StatusCode::OK);
let status_body: Value = repo_status_res.json().await.unwrap();
assert_eq!(status_body["did"], did);
assert_eq!(status_body["active"], true);
let get_repo_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getRepo",
base_url().await
))
.query(&[("did", did.as_str())])
.send()
.await
.expect("Failed to get full repo");
assert_eq!(get_repo_res.status(), StatusCode::OK);
assert_eq!(
get_repo_res
.headers()
.get("content-type")
.and_then(|h| h.to_str().ok()),
Some("application/vnd.ipld.car")
);
let repo_car = get_repo_res.bytes().await.unwrap();
assert!(repo_car.len() > 100, "Repo CAR should have substantial data");
let list_blobs_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.listBlobs",
base_url().await
))
.query(&[("did", did.as_str())])
.send()
.await
.expect("Failed to list blobs");
assert_eq!(list_blobs_res.status(), StatusCode::OK);
let blobs_body: Value = list_blobs_res.json().await.unwrap();
let cids = blobs_body["cids"].as_array().unwrap();
assert!(!cids.is_empty(), "Should have at least one blob");
let get_blob_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getBlob",
base_url().await
))
.query(&[("did", did.as_str()), ("cid", &blob_cid)])
.send()
.await
.expect("Failed to get blob");
assert_eq!(get_blob_res.status(), StatusCode::OK);
let retrieved_blob = get_blob_res.bytes().await.unwrap();
assert_eq!(
retrieved_blob.as_ref(),
blob_data,
"Retrieved blob should match uploaded data"
);
let latest_commit_res = client
.get(format!(
"{}/xrpc/com.atproto.sync.getLatestCommit",
base_url().await
))
.query(&[("did", did.as_str())])
.send()
.await
.expect("Failed to get latest commit");
assert_eq!(latest_commit_res.status(), StatusCode::OK);
let commit_body: Value = latest_commit_res.json().await.unwrap();
let root_cid = commit_body["cid"].as_str().unwrap();
let get_blocks_url = format!(
"{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}",
base_url().await,
did,
root_cid
);
let get_blocks_res = client
.get(&get_blocks_url)
.send()
.await
.expect("Failed to get blocks");
assert_eq!(get_blocks_res.status(), StatusCode::OK);
assert_eq!(
get_blocks_res
.headers()
.get("content-type")
.and_then(|h| h.to_str().ok()),
Some("application/vnd.ipld.car")
);
}