use super::*;
async fn rejected<S: AuthSchema>(
ctx: &AuthContext<S>,
cookie: &str,
id: &str,
expected: (u16, Option<Value>),
) -> TestResult {
for path in ["/passkey/update-passkey", "/passkey/delete-passkey"] {
let response = response(
ctx,
&request(
path,
None,
Some(json!({"id": id, "name": "Unauthorized"})),
Some(cookie),
),
)
.await;
assert_eq!(response.status, expected.0);
if let Some(expected) = &expected.1 {
assert_eq!(
serde_json::from_slice::<Value>(&response.body.bytes()?)?,
*expected
);
} else {
assert!(response.body.bytes()?.is_empty());
}
}
Ok(())
}
async fn owner_operations<S: AuthSchema>(ctx: AuthContext<S>, sqlite: bool) -> TestResult {
let token = owner_session(&ctx).await?;
let original = existing_passkey(&ctx).await?;
for sample in cases()? {
if sqlite && !sample.contains_key("sqliteMatches") {
continue;
}
let owner = replacement(&sample);
if owner.strict_equals(&FieldValue::from("7")) {
continue;
}
let (projected, cookie) = project(&ctx, owner.clone(), &token, None).await?;
if !owner.is_truthy() {
for id in [original.id.typed()?, "missing", ""] {
rejected(&projected, &cookie, id, (401, None)).await?;
}
} else {
let update = response(
&projected,
&request(
"/passkey/update-passkey",
None,
Some(json!({"id": original.id, "name": "Unauthorized"})),
Some(&cookie),
),
)
.await;
assert_eq!(update.status, 401, "{sample:?}");
assert_eq!(
serde_json::from_slice::<Value>(&update.body.bytes()?)?,
json!({
"code": "YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY",
"message": "You are not allowed to register this passkey"
})
);
let delete = response(
&projected,
&request(
"/passkey/delete-passkey",
None,
Some(json!({"id": original.id})),
Some(&cookie),
),
)
.await;
assert_eq!(delete.status, 401, "{sample:?}");
assert!(delete.body.bytes()?.is_empty());
}
assert_eq!(
ctx.database.get_passkey_by_id(original.id.typed()?).await?,
Some(original.clone())
);
}
let (projected, cookie) = project(&ctx, "7".into(), &token, None).await?;
rejected(
&projected,
&cookie,
"",
(
400,
Some(json!({
"message": "Missing required parameter: id"
})),
),
)
.await?;
rejected(
&projected,
&cookie,
"missing",
(
404,
Some(json!({
"code": "PASSKEY_NOT_FOUND", "message": "Passkey not found"
})),
),
)
.await?;
let started = chrono::Utc::now().timestamp_millis() as f64;
let updated = response(
&projected,
&request(
"/passkey/update-passkey",
None,
Some(json!({"id": original.id, "name": "Renamed"})),
Some(&cookie),
),
)
.await;
assert_eq!(updated.status, 200);
let stored = ctx
.database
.get_passkey_by_id(original.id.typed()?)
.await?
.ok_or_else(|| AuthError::internal("Expected updated passkey"))?;
match ctx.database.passkey_storage() {
better_auth_core::PasskeyStorage::Native => {
assert!(stored.updated_at.field_value().is_undefined());
}
better_auth_core::PasskeyStorage::Legacy => assert!(
(started..=chrono::Utc::now().timestamp_millis() as f64)
.contains(&stored.updated_at.date_milliseconds()?)
),
}
let mut expected = original.clone();
expected.name = Some("Renamed".to_owned()).into();
expected.updated_at = stored.updated_at.clone();
assert_eq!(
serde_json::from_slice::<Value>(&updated.body.bytes()?)?,
json!({"passkey": PasskeyView::from(&expected)})
);
assert_eq!(stored, expected);
let deleted = response(
&projected,
&request(
"/passkey/delete-passkey",
None,
Some(json!({"id": original.id})),
Some(&cookie),
),
)
.await;
assert_eq!(deleted.status, 200);
assert_eq!(
serde_json::from_slice::<Value>(&deleted.body.bytes()?)?,
json!({"status": true})
);
assert!(
ctx.database
.get_passkey_by_id(original.id.typed()?)
.await?
.is_none()
);
Ok(())
}
#[tokio::test]
async fn memory_owner_operations_enforce_truthiness_and_strict_equality() -> TestResult {
owner_operations(memory(), false).await
}
#[tokio::test]
async fn sqlite_owner_operations_compare_returned_owner_without_coercion() -> TestResult {
owner_operations(
test_helpers::create_test_context_with_config(
test_helpers::create_test_config().base_url(ORIGIN),
)
.await,
true,
)
.await
}
#[tokio::test]
async fn equal_object_contents_do_not_authorize_passkey_mutations() -> TestResult {
let ctx = memory();
let token = owner_session(&ctx).await?;
let original = existing_passkey(&ctx).await?;
let owner = FieldValue::from(FieldMap::from([("owner".into(), 7.0.into())]));
let (projected, cookie) = project(&ctx, owner.clone(), &token, Some(owner)).await?;
let update = response(
&projected,
&request(
"/passkey/update-passkey",
None,
Some(json!({"id": original.id, "name": "Unauthorized"})),
Some(&cookie),
),
)
.await;
assert_eq!(update.status, 401);
assert_eq!(
serde_json::from_slice::<Value>(&update.body.bytes()?)?,
json!({
"code": "YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY",
"message": "You are not allowed to register this passkey"
})
);
let delete = response(
&projected,
&request(
"/passkey/delete-passkey",
None,
Some(json!({"id": original.id})),
Some(&cookie),
),
)
.await;
assert_eq!(delete.status, 401);
assert!(delete.body.bytes()?.is_empty());
assert_eq!(
ctx.database.get_passkey_by_id(original.id.typed()?).await?,
Some(original)
);
Ok(())
}