use serde_json::Value;
use sqlx::Row;
use sqlx::sqlite::SqliteRow;
use tracing::{debug, info};
use uuid::Uuid;
use crate::random::random_token;
use crate::sqlite::db::Database;
use crate::sqlite::nonce::now_secs;
use crate::sqlite::order::{Identifier, rfc3339};
use crate::sqlite::status::{self, AuthzStatus, ChallengeStatus};
#[derive(Debug)]
pub struct Authorization {
pub id: Uuid,
pub order_id: Uuid,
pub identifier: Identifier,
pub status: AuthzStatus,
pub expires: i64,
pub created_at: i64,
}
#[derive(Debug)]
pub struct Challenge {
pub id: Uuid,
pub authz_id: Uuid,
pub typ: String,
pub token: String,
pub status: ChallengeStatus,
pub validated: Option<i64>,
pub error: Option<Value>,
pub created_at: i64,
}
macro_rules! authz_columns {
() => {
"id, order_id, identifier, status, expires, created_at"
};
}
impl Authorization {
fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
let identifier_json: String = row.try_get("identifier")?;
let identifier: Identifier =
serde_json::from_str(&identifier_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
Ok(Authorization {
id: row.try_get("id")?,
order_id: row.try_get("order_id")?,
identifier,
status: status::from_column(row.try_get::<&str, _>("status")?)?,
expires: row.try_get("expires")?,
created_at: row.try_get("created_at")?,
})
}
pub(crate) fn new(order_id: Uuid, identifier: Identifier, expires: i64) -> Authorization {
Authorization {
id: crate::sqlite::id::mint(),
order_id,
identifier,
status: AuthzStatus::Pending,
expires,
created_at: now_secs(),
}
}
pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
let identifier_json = serde_json::to_string(&self.identifier)
.map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
debug!(event = "db_authz_create_started", outcome = "progress", authz_id = ?self.id, order_id = ?self.order_id);
sqlx::query(
"INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) \
VALUES (?, ?, ?, ?, ?, ?);",
)
.bind(self.id)
.bind(self.order_id)
.bind(identifier_json)
.bind(self.status.as_str())
.bind(self.expires)
.bind(self.created_at)
.execute(executor)
.await?;
info!(event = "db_authz_created", outcome = "success", authz_id = ?self.id, order_id = ?self.order_id);
Ok(())
}
pub async fn create(
order_id: Uuid,
identifier: Identifier,
expires: i64,
database: &Database,
) -> Result<Authorization, sqlx::Error> {
let authz = Authorization::new(order_id, identifier, expires);
authz.insert(&database.pool).await?;
Ok(authz)
}
pub async fn find_by_id(
id: &str,
database: &Database,
) -> Result<Option<Authorization>, sqlx::Error> {
debug!(event = "db_authz_find_by_id_started", outcome = "progress", authz_id = ?id);
let Some(id) = crate::sqlite::id::parse(id) else {
return Ok(None);
};
let row = sqlx::query(concat!(
"SELECT ",
authz_columns!(),
" FROM authorizations WHERE id = ?;"
))
.bind(id)
.fetch_optional(&database.pool)
.await?;
row.map(Authorization::from_row).transpose()
}
pub async fn find_by_order(
order_id: Uuid,
database: &Database,
) -> Result<Vec<Authorization>, sqlx::Error> {
Self::find_by_order_with(order_id, &database.pool).await
}
pub async fn count_by_order(order_id: Uuid, database: &Database) -> Result<i64, sqlx::Error> {
let row = sqlx::query("SELECT COUNT(*) FROM authorizations WHERE order_id = ?;")
.bind(order_id)
.fetch_one(&database.pool)
.await?;
row.try_get::<i64, _>(0)
}
pub async fn find_ids_by_orders(
order_ids: &[Uuid],
database: &Database,
) -> Result<std::collections::HashMap<Uuid, Vec<Uuid>>, sqlx::Error> {
let mut grouped: std::collections::HashMap<Uuid, Vec<Uuid>> =
std::collections::HashMap::new();
if order_ids.is_empty() {
return Ok(grouped);
}
let mut builder =
sqlx::QueryBuilder::new("SELECT id, order_id FROM authorizations WHERE order_id IN (");
let mut separated = builder.separated(", ");
for id in order_ids {
separated.push_bind(*id);
}
builder.push(") ORDER BY created_at ASC;");
debug!(
event = "db_authz_find_ids_by_orders",
outcome = "success",
orders = order_ids.len()
);
for row in builder.build().fetch_all(&database.pool).await? {
let order_id: Uuid = row.try_get("order_id")?;
let id: Uuid = row.try_get("id")?;
grouped.entry(order_id).or_default().push(id);
}
Ok(grouped)
}
pub(crate) async fn find_by_order_with<'e, E>(
order_id: Uuid,
executor: E,
) -> Result<Vec<Authorization>, sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
debug!(event = "db_authz_find_by_order_started", outcome = "progress", order_id = ?order_id);
let rows = sqlx::query(concat!(
"SELECT ",
authz_columns!(),
" FROM authorizations WHERE order_id = ? ORDER BY created_at ASC;"
))
.bind(order_id)
.fetch_all(executor)
.await?;
rows.into_iter().map(Authorization::from_row).collect()
}
pub(crate) async fn set_valid<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query("UPDATE authorizations SET status = 'valid' WHERE id = ?;")
.bind(id)
.execute(executor)
.await?;
Ok(())
}
pub(crate) async fn set_invalid<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query("UPDATE authorizations SET status = 'invalid' WHERE id = ?;")
.bind(id)
.execute(executor)
.await?;
Ok(())
}
pub(crate) async fn set_deactivated<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query("UPDATE authorizations SET status = 'deactivated' WHERE id = ?;")
.bind(id)
.execute(executor)
.await?;
Ok(())
}
pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
debug!(event = "db_authz_mark_valid_started", outcome = "progress", authz_id = ?self.id);
Self::set_valid(self.id, &database.pool).await?;
self.status = AuthzStatus::Valid;
info!(event = "db_authz_marked_valid", outcome = "success", authz_id = ?self.id);
Ok(())
}
pub async fn mark_invalid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
debug!(event = "db_authz_mark_invalid_started", outcome = "progress", authz_id = ?self.id);
Self::set_invalid(self.id, &database.pool).await?;
self.status = AuthzStatus::Invalid;
info!(event = "db_authz_marked_invalid", outcome = "failure", authz_id = ?self.id);
Ok(())
}
#[must_use]
pub fn is_wildcard(&self) -> bool {
self.identifier.value.starts_with("*.")
}
#[must_use]
pub fn base_identifier(&self) -> &str {
self.identifier
.value
.strip_prefix("*.")
.unwrap_or(&self.identifier.value)
}
#[must_use]
pub fn to_json(&self, base_url: &str, challenges: &[Challenge]) -> Value {
let mut object = serde_json::Map::new();
object.insert(
"identifier".to_string(),
serde_json::to_value(Identifier::new(
self.identifier.typ.clone(),
self.base_identifier().to_string(),
))
.expect("Identifier is always serializable"),
);
object.insert(
"status".to_string(),
Value::String(self.status.as_str().to_string()),
);
object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
let challenges: Vec<Value> = challenges.iter().map(|c| c.to_json(base_url)).collect();
object.insert("challenges".to_string(), Value::Array(challenges));
if self.is_wildcard() {
object.insert("wildcard".to_string(), Value::Bool(true));
}
Value::Object(object)
}
}
macro_rules! challenge_columns {
() => {
"id, authz_id, type, token, status, validated, error, created_at"
};
}
impl Challenge {
fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
let error_json: Option<String> = row.try_get("error")?;
let error = error_json
.map(|json| serde_json::from_str(&json))
.transpose()
.map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
Ok(Challenge {
id: row.try_get("id")?,
authz_id: row.try_get("authz_id")?,
typ: row.try_get("type")?,
token: row.try_get("token")?,
status: status::from_column(row.try_get::<&str, _>("status")?)?,
validated: row.try_get("validated")?,
error,
created_at: row.try_get("created_at")?,
})
}
pub(crate) fn new(authz_id: Uuid, typ: &str) -> Challenge {
Challenge {
id: crate::sqlite::id::mint(),
authz_id,
typ: typ.to_string(),
token: random_token(),
status: ChallengeStatus::Pending,
validated: None,
error: None,
created_at: now_secs(),
}
}
pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
debug!(event = "db_challenge_create_started", outcome = "progress", challenge_id = ?self.id, authz_id = ?self.authz_id);
sqlx::query(
"INSERT INTO challenges (id, authz_id, type, token, status, validated, created_at) \
VALUES (?, ?, ?, ?, ?, NULL, ?);",
)
.bind(self.id)
.bind(self.authz_id)
.bind(&self.typ)
.bind(&self.token)
.bind(self.status.as_str())
.bind(self.created_at)
.execute(executor)
.await?;
info!(event = "db_challenge_created", outcome = "success", challenge_id = ?self.id, authz_id = ?self.authz_id);
Ok(())
}
pub async fn create(
authz_id: Uuid,
typ: &str,
database: &Database,
) -> Result<Challenge, sqlx::Error> {
let challenge = Challenge::new(authz_id, typ);
challenge.insert(&database.pool).await?;
Ok(challenge)
}
pub async fn find_by_id(
id: &str,
database: &Database,
) -> Result<Option<Challenge>, sqlx::Error> {
debug!(event = "db_challenge_find_by_id_started", outcome = "progress", challenge_id = ?id);
let Some(id) = crate::sqlite::id::parse(id) else {
return Ok(None);
};
let row = sqlx::query(concat!(
"SELECT ",
challenge_columns!(),
" FROM challenges WHERE id = ?;"
))
.bind(id)
.fetch_optional(&database.pool)
.await?;
row.map(Challenge::from_row).transpose()
}
pub async fn find_by_authz(
authz_id: Uuid,
database: &Database,
) -> Result<Vec<Challenge>, sqlx::Error> {
debug!(event = "db_challenge_find_by_authz_started", outcome = "progress", authz_id = ?authz_id);
let rows = sqlx::query(concat!(
"SELECT ",
challenge_columns!(),
" FROM challenges WHERE authz_id = ? ORDER BY created_at ASC;"
))
.bind(authz_id)
.fetch_all(&database.pool)
.await?;
rows.into_iter().map(Challenge::from_row).collect()
}
pub async fn claim_for_validation(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
debug!(event = "db_challenge_claim_started", outcome = "progress", challenge_id = ?self.id);
let claimed = sqlx::query(
"UPDATE challenges SET status = 'processing' WHERE id = ? AND status = 'pending';",
)
.bind(self.id)
.execute(&database.pool)
.await?
.rows_affected()
== 1;
if !claimed {
debug!(event = "db_challenge_claim_refused", outcome = "advisory", challenge_id = ?self.id);
return Ok(false);
}
self.status = ChallengeStatus::Processing;
debug!(event = "db_challenge_claimed", outcome = "success", challenge_id = ?self.id);
Ok(true)
}
pub(crate) async fn set_valid<'e, E>(
id: Uuid,
validated: i64,
executor: E,
) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query("UPDATE challenges SET status = 'valid', validated = ? WHERE id = ?;")
.bind(validated)
.bind(id)
.execute(executor)
.await?;
Ok(())
}
pub(crate) async fn set_invalid<'e, E>(
id: Uuid,
error: &Value,
executor: E,
) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
let error_json =
serde_json::to_string(error).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
sqlx::query("UPDATE challenges SET status = 'invalid', error = ? WHERE id = ?;")
.bind(error_json)
.bind(id)
.execute(executor)
.await?;
Ok(())
}
pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
let validated = now_secs();
debug!(event = "db_challenge_mark_valid_started", outcome = "progress", challenge_id = ?self.id);
Self::set_valid(self.id, validated, &database.pool).await?;
self.status = ChallengeStatus::Valid;
self.validated = Some(validated);
info!(event = "db_challenge_marked_valid", outcome = "success", challenge_id = ?self.id);
Ok(())
}
pub async fn mark_invalid(
&mut self,
error: Value,
database: &Database,
) -> Result<(), sqlx::Error> {
debug!(event = "db_challenge_mark_invalid_started", outcome = "progress", challenge_id = ?self.id);
Self::set_invalid(self.id, &error, &database.pool).await?;
self.status = ChallengeStatus::Invalid;
self.error = Some(error);
info!(event = "db_challenge_marked_invalid", outcome = "failure", challenge_id = ?self.id);
Ok(())
}
#[must_use]
pub fn to_json(&self, base_url: &str) -> Value {
let mut object = serde_json::Map::new();
object.insert("type".to_string(), Value::String(self.typ.clone()));
object.insert(
"url".to_string(),
Value::String(format!("{base_url}/chall/{}", self.id)),
);
object.insert(
"status".to_string(),
Value::String(self.status.as_str().to_string()),
);
object.insert("token".to_string(), Value::String(self.token.clone()));
if let Some(validated) = self.validated {
object.insert("validated".to_string(), Value::String(rfc3339(validated)));
}
if let Some(error) = &self.error {
object.insert("error".to_string(), error.clone());
}
Value::Object(object)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audit::ClientContext;
use crate::sqlite::account::Account;
use crate::sqlite::order::Order;
use crate::sqlite::status::OrderStatus;
use crate::testutil::account_id;
use std::sync::Arc;
#[tokio::test]
async fn ids_for_several_orders_come_back_grouped_in_one_query() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let account = account_id(&db).await;
let mut expected = Vec::new();
for name in ["a.example.com", "b.example.com"] {
let order = Order::create(
"default",
account,
vec![Identifier::dns(name)],
now_secs() + 3600,
None,
None,
&db,
)
.await
.unwrap();
let first =
Authorization::create(order.id, Identifier::dns(name), now_secs() + 3600, &db)
.await
.unwrap();
expected.push((order.id, first.id));
}
let ids: Vec<Uuid> = expected.iter().map(|(o, _)| *o).collect();
let grouped = Authorization::find_ids_by_orders(&ids, &db).await.unwrap();
assert_eq!(grouped.len(), 2);
for (order_id, authz_id) in &expected {
assert_eq!(grouped[order_id], vec![*authz_id]);
}
let grouped = Authorization::find_ids_by_orders(&[crate::sqlite::id::mint()], &db)
.await
.unwrap();
assert!(grouped.is_empty());
assert!(
Authorization::find_ids_by_orders(&[], &db)
.await
.unwrap()
.is_empty()
);
}
async fn order_id(db: &Arc<Database>) -> String {
let (account, _) = Account::find_or_create(
"default",
&[1u8, 2, 3],
vec![],
&ClientContext::default(),
db,
)
.await
.unwrap();
let order = Order::create(
"default",
account.id,
vec![Identifier::dns("example.com")],
now_secs() + 3600,
None,
None,
db,
)
.await
.unwrap();
order.id.to_string()
}
#[tokio::test]
async fn the_validation_transitions_roll_back_together() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
let mut tx = db.pool.begin().await.unwrap();
Challenge::set_valid(challenge.id, now_secs(), &mut *tx)
.await
.unwrap();
Authorization::set_valid(authz.id, &mut *tx).await.unwrap();
Order::set_ready(oid.parse().unwrap(), &mut *tx)
.await
.unwrap();
tx.rollback().await.unwrap();
let reloaded_authz = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
let reloaded_challenge = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
let reloaded_order = Order::find_by_id(&oid, &db).await.unwrap().unwrap();
assert_eq!(reloaded_challenge.status, ChallengeStatus::Pending);
assert_eq!(reloaded_authz.status, AuthzStatus::Pending);
assert_eq!(reloaded_order.status, OrderStatus::Pending);
}
#[tokio::test]
async fn the_validation_transitions_commit_together() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
let mut tx = db.pool.begin().await.unwrap();
Challenge::set_valid(challenge.id, now_secs(), &mut *tx)
.await
.unwrap();
Authorization::set_valid(authz.id, &mut *tx).await.unwrap();
Order::set_ready(oid.parse().unwrap(), &mut *tx)
.await
.unwrap();
tx.commit().await.unwrap();
assert_eq!(
Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap()
.status,
ChallengeStatus::Valid
);
assert_eq!(
Authorization::find_by_id(authz.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap()
.status,
AuthzStatus::Valid
);
assert_eq!(
Order::find_by_id(&oid, &db).await.unwrap().unwrap().status,
OrderStatus::Ready
);
}
#[tokio::test]
async fn authz_create_find_round_trip() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
assert_eq!(authz.status, AuthzStatus::Pending);
let by_id = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(by_id.identifier, Identifier::dns("example.com"));
assert_eq!(by_id.order_id.to_string(), oid);
let by_order = Authorization::find_by_order(oid.parse().unwrap(), &db)
.await
.unwrap();
assert_eq!(by_order.len(), 1);
}
#[tokio::test]
async fn authz_mark_valid_persists_and_syncs() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let mut authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
authz.mark_valid(&db).await.unwrap();
assert_eq!(authz.status, AuthzStatus::Valid);
let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(reloaded.status, AuthzStatus::Valid);
}
#[tokio::test]
async fn authz_to_json_shape() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
let json = authz.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
assert_eq!(json["status"], "pending");
assert_eq!(
json["identifier"],
serde_json::json!({"type":"dns","value":"example.com"})
);
assert!(json["expires"].as_str().unwrap().ends_with('Z'));
assert_eq!(json["challenges"].as_array().unwrap().len(), 1);
assert_eq!(json["challenges"][0]["type"], "http-01");
}
#[tokio::test]
async fn challenge_create_find_round_trip() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
assert_eq!(challenge.typ, "http-01");
assert_eq!(challenge.status, ChallengeStatus::Pending);
assert!(!challenge.token.is_empty());
assert!(challenge.validated.is_none());
let by_id = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(by_id.token, challenge.token);
let by_authz = Challenge::find_by_authz(authz.id, &db).await.unwrap();
assert_eq!(by_authz.len(), 1);
}
#[tokio::test]
async fn challenge_mark_valid_persists_and_syncs() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
challenge.mark_valid(&db).await.unwrap();
assert_eq!(challenge.status, ChallengeStatus::Valid);
assert!(challenge.validated.is_some());
let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(reloaded.status, ChallengeStatus::Valid);
let json = reloaded.to_json("http://localhost:3000");
assert_eq!(json["status"], "valid");
assert_eq!(
json["url"],
format!("http://localhost:3000/chall/{}", challenge.id)
);
assert!(json["validated"].as_str().unwrap().ends_with('Z'));
}
#[tokio::test]
async fn challenge_mark_invalid_persists_the_problem_document() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
let problem = serde_json::json!({
"type": "urn:ietf:params:acme:error:incorrectResponse",
"detail": "response body does not match the key authorization",
"status": 403,
});
challenge.mark_invalid(problem.clone(), &db).await.unwrap();
assert_eq!(challenge.status, ChallengeStatus::Invalid);
assert_eq!(challenge.error.as_ref(), Some(&problem));
assert!(challenge.validated.is_none());
let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(reloaded.status, ChallengeStatus::Invalid);
assert_eq!(reloaded.error.as_ref(), Some(&problem));
let json = reloaded.to_json("http://localhost:3000");
assert_eq!(json["error"], problem);
assert!(json.get("validated").is_none());
}
#[tokio::test]
async fn authz_mark_invalid_persists_and_syncs() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let mut authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
authz.mark_invalid(&db).await.unwrap();
assert_eq!(authz.status, AuthzStatus::Invalid);
let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(reloaded.status, AuthzStatus::Invalid);
}
#[tokio::test]
async fn an_authorization_holds_one_challenge_per_type() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let http = Challenge::create(authz.id, "http-01", &db).await.unwrap();
let dns_01 = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
Challenge::create(authz.id, "tls-alpn-01", &db)
.await
.unwrap();
assert_ne!(http.token, dns_01.token);
let challenges = Challenge::find_by_authz(authz.id, &db).await.unwrap();
assert_eq!(challenges.len(), 3);
assert!(Challenge::create(authz.id, "http-01", &db).await.is_err());
}
#[tokio::test]
async fn a_wildcard_authorization_stores_the_prefix_and_renders_the_base_name() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("*.example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
let challenge = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert_eq!(reloaded.identifier.value, "*.example.com");
assert!(reloaded.is_wildcard());
assert_eq!(reloaded.base_identifier(), "example.com");
let json = reloaded.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
assert_eq!(
json["identifier"],
serde_json::json!({"type":"dns","value":"example.com"})
);
assert_eq!(json["wildcard"], true);
assert_eq!(json["challenges"][0]["type"], "dns-01");
}
#[tokio::test]
async fn a_plain_authorization_has_no_wildcard_member() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
let oid = order_id(&db).await;
let authz = Authorization::create(
oid.parse().unwrap(),
Identifier::dns("example.com"),
now_secs() + 3600,
&db,
)
.await
.unwrap();
assert!(!authz.is_wildcard());
assert_eq!(authz.base_identifier(), "example.com");
assert!(
authz
.to_json("http://localhost:3000", &[])
.get("wildcard")
.is_none()
);
}
#[tokio::test]
async fn absent_lookups_return_none() {
let db = Arc::new(Database::connect_in_memory().await.unwrap());
assert!(
Authorization::find_by_id("nope", &db)
.await
.unwrap()
.is_none()
);
assert!(Challenge::find_by_id("nope", &db).await.unwrap().is_none());
}
}