1use serde_json::Value;
2use sqlx::Row;
3use sqlx::sqlite::SqliteRow;
4use tracing::{debug, info};
5use uuid::Uuid;
6
7use crate::random::random_token;
8use crate::sqlite::db::Database;
9use crate::sqlite::nonce::now_secs;
10use crate::sqlite::order::{Identifier, rfc3339};
11use crate::sqlite::status::{self, AuthzStatus, ChallengeStatus};
12
13#[derive(Debug)]
37pub struct Authorization {
38 pub id: Uuid,
39 pub order_id: Uuid,
40 pub identifier: Identifier,
41 pub status: AuthzStatus,
42 pub expires: i64,
43 pub created_at: i64,
44}
45
46#[derive(Debug)]
58pub struct Challenge {
59 pub id: Uuid,
60 pub authz_id: Uuid,
61 pub typ: String,
62 pub token: String,
63 pub status: ChallengeStatus,
64 pub validated: Option<i64>,
65 pub error: Option<Value>,
68 pub created_at: i64,
69}
70
71macro_rules! authz_columns {
77 () => {
78 "id, order_id, identifier, status, expires, created_at"
79 };
80}
81
82impl Authorization {
83 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
84 let identifier_json: String = row.try_get("identifier")?;
85 let identifier: Identifier =
86 serde_json::from_str(&identifier_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
87
88 Ok(Authorization {
89 id: row.try_get("id")?,
90 order_id: row.try_get("order_id")?,
91 identifier,
92 status: status::from_column(row.try_get::<&str, _>("status")?)?,
93 expires: row.try_get("expires")?,
94 created_at: row.try_get("created_at")?,
95 })
96 }
97
98 pub(crate) fn new(order_id: Uuid, identifier: Identifier, expires: i64) -> Authorization {
101 Authorization {
102 id: crate::sqlite::id::mint(),
103 order_id,
104 identifier,
105 status: AuthzStatus::Pending,
106 expires,
107 created_at: now_secs(),
108 }
109 }
110
111 pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
114 where
115 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
116 {
117 let identifier_json = serde_json::to_string(&self.identifier)
119 .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
120
121 debug!(event = "db_authz_create_started", outcome = "progress", authz_id = ?self.id, order_id = ?self.order_id);
122 sqlx::query(
123 "INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) \
124 VALUES (?, ?, ?, ?, ?, ?);",
125 )
126 .bind(self.id)
127 .bind(self.order_id)
128 .bind(identifier_json)
129 .bind(self.status.as_str())
130 .bind(self.expires)
131 .bind(self.created_at)
132 .execute(executor)
133 .await?;
134
135 info!(event = "db_authz_created", outcome = "success", authz_id = ?self.id, order_id = ?self.order_id);
136 Ok(())
137 }
138
139 pub async fn create(
141 order_id: Uuid,
142 identifier: Identifier,
143 expires: i64,
144 database: &Database,
145 ) -> Result<Authorization, sqlx::Error> {
146 let authz = Authorization::new(order_id, identifier, expires);
147 authz.insert(&database.pool).await?;
148 Ok(authz)
149 }
150
151 pub async fn find_by_id(
152 id: &str,
153 database: &Database,
154 ) -> Result<Option<Authorization>, sqlx::Error> {
155 debug!(event = "db_authz_find_by_id_started", outcome = "progress", authz_id = ?id);
156 let Some(id) = crate::sqlite::id::parse(id) else {
157 return Ok(None);
158 };
159 let row = sqlx::query(concat!(
160 "SELECT ",
161 authz_columns!(),
162 " FROM authorizations WHERE id = ?;"
163 ))
164 .bind(id)
165 .fetch_optional(&database.pool)
166 .await?;
167
168 row.map(Authorization::from_row).transpose()
169 }
170
171 pub async fn find_by_order(
174 order_id: Uuid,
175 database: &Database,
176 ) -> Result<Vec<Authorization>, sqlx::Error> {
177 Self::find_by_order_with(order_id, &database.pool).await
178 }
179
180 pub async fn count_by_order(order_id: Uuid, database: &Database) -> Result<i64, sqlx::Error> {
183 let row = sqlx::query("SELECT COUNT(*) FROM authorizations WHERE order_id = ?;")
184 .bind(order_id)
185 .fetch_one(&database.pool)
186 .await?;
187 row.try_get::<i64, _>(0)
188 }
189
190 pub async fn find_ids_by_orders(
200 order_ids: &[Uuid],
201 database: &Database,
202 ) -> Result<std::collections::HashMap<Uuid, Vec<Uuid>>, sqlx::Error> {
203 let mut grouped: std::collections::HashMap<Uuid, Vec<Uuid>> =
204 std::collections::HashMap::new();
205 if order_ids.is_empty() {
206 return Ok(grouped);
207 }
208
209 let mut builder =
213 sqlx::QueryBuilder::new("SELECT id, order_id FROM authorizations WHERE order_id IN (");
214 let mut separated = builder.separated(", ");
215 for id in order_ids {
216 separated.push_bind(*id);
217 }
218 builder.push(") ORDER BY created_at ASC;");
219
220 debug!(
221 event = "db_authz_find_ids_by_orders",
222 outcome = "success",
223 orders = order_ids.len()
224 );
225 for row in builder.build().fetch_all(&database.pool).await? {
226 let order_id: Uuid = row.try_get("order_id")?;
227 let id: Uuid = row.try_get("id")?;
228 grouped.entry(order_id).or_default().push(id);
229 }
230 Ok(grouped)
231 }
232
233 pub(crate) async fn find_by_order_with<'e, E>(
242 order_id: Uuid,
243 executor: E,
244 ) -> Result<Vec<Authorization>, sqlx::Error>
245 where
246 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
247 {
248 debug!(event = "db_authz_find_by_order_started", outcome = "progress", order_id = ?order_id);
249 let rows = sqlx::query(concat!(
250 "SELECT ",
251 authz_columns!(),
252 " FROM authorizations WHERE order_id = ? ORDER BY created_at ASC;"
253 ))
254 .bind(order_id)
255 .fetch_all(executor)
256 .await?;
257
258 rows.into_iter().map(Authorization::from_row).collect()
259 }
260
261 pub(crate) async fn set_valid<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
269 where
270 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
271 {
272 sqlx::query("UPDATE authorizations SET status = 'valid' WHERE id = ?;")
273 .bind(id)
274 .execute(executor)
275 .await?;
276 Ok(())
277 }
278
279 pub(crate) async fn set_invalid<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
281 where
282 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
283 {
284 sqlx::query("UPDATE authorizations SET status = 'invalid' WHERE id = ?;")
285 .bind(id)
286 .execute(executor)
287 .await?;
288 Ok(())
289 }
290
291 pub(crate) async fn set_deactivated<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
302 where
303 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
304 {
305 sqlx::query("UPDATE authorizations SET status = 'deactivated' WHERE id = ?;")
306 .bind(id)
307 .execute(executor)
308 .await?;
309 Ok(())
310 }
311
312 pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
315 debug!(event = "db_authz_mark_valid_started", outcome = "progress", authz_id = ?self.id);
316 Self::set_valid(self.id, &database.pool).await?;
317
318 self.status = AuthzStatus::Valid;
319 info!(event = "db_authz_marked_valid", outcome = "success", authz_id = ?self.id);
320 Ok(())
321 }
322
323 pub async fn mark_invalid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
330 debug!(event = "db_authz_mark_invalid_started", outcome = "progress", authz_id = ?self.id);
331 Self::set_invalid(self.id, &database.pool).await?;
332
333 self.status = AuthzStatus::Invalid;
334 info!(event = "db_authz_marked_invalid", outcome = "failure", authz_id = ?self.id);
335 Ok(())
336 }
337
338 #[must_use]
343 pub fn is_wildcard(&self) -> bool {
344 self.identifier.value.starts_with("*.")
345 }
346
347 #[must_use]
353 pub fn base_identifier(&self) -> &str {
354 self.identifier
355 .value
356 .strip_prefix("*.")
357 .unwrap_or(&self.identifier.value)
358 }
359
360 #[must_use]
364 pub fn to_json(&self, base_url: &str, challenges: &[Challenge]) -> Value {
365 let mut object = serde_json::Map::new();
366 object.insert(
370 "identifier".to_string(),
371 serde_json::to_value(Identifier::new(
372 self.identifier.typ.clone(),
373 self.base_identifier().to_string(),
374 ))
375 .expect("Identifier is always serializable"),
376 );
377 object.insert(
378 "status".to_string(),
379 Value::String(self.status.as_str().to_string()),
380 );
381 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
382 let challenges: Vec<Value> = challenges.iter().map(|c| c.to_json(base_url)).collect();
383 object.insert("challenges".to_string(), Value::Array(challenges));
384 if self.is_wildcard() {
385 object.insert("wildcard".to_string(), Value::Bool(true));
386 }
387 Value::Object(object)
388 }
389}
390
391macro_rules! challenge_columns {
397 () => {
398 "id, authz_id, type, token, status, validated, error, created_at"
399 };
400}
401
402impl Challenge {
403 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
404 let error_json: Option<String> = row.try_get("error")?;
405 let error = error_json
406 .map(|json| serde_json::from_str(&json))
407 .transpose()
408 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
409
410 Ok(Challenge {
411 id: row.try_get("id")?,
412 authz_id: row.try_get("authz_id")?,
413 typ: row.try_get("type")?,
414 token: row.try_get("token")?,
415 status: status::from_column(row.try_get::<&str, _>("status")?)?,
416 validated: row.try_get("validated")?,
417 error,
418 created_at: row.try_get("created_at")?,
419 })
420 }
421
422 pub(crate) fn new(authz_id: Uuid, typ: &str) -> Challenge {
429 Challenge {
430 id: crate::sqlite::id::mint(),
431 authz_id,
432 typ: typ.to_string(),
433 token: random_token(),
434 status: ChallengeStatus::Pending,
435 validated: None,
436 error: None,
437 created_at: now_secs(),
438 }
439 }
440
441 pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
444 where
445 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
446 {
447 debug!(event = "db_challenge_create_started", outcome = "progress", challenge_id = ?self.id, authz_id = ?self.authz_id);
448 sqlx::query(
449 "INSERT INTO challenges (id, authz_id, type, token, status, validated, created_at) \
450 VALUES (?, ?, ?, ?, ?, NULL, ?);",
451 )
452 .bind(self.id)
453 .bind(self.authz_id)
454 .bind(&self.typ)
455 .bind(&self.token)
456 .bind(self.status.as_str())
457 .bind(self.created_at)
458 .execute(executor)
459 .await?;
460
461 info!(event = "db_challenge_created", outcome = "success", challenge_id = ?self.id, authz_id = ?self.authz_id);
462 Ok(())
463 }
464
465 pub async fn create(
468 authz_id: Uuid,
469 typ: &str,
470 database: &Database,
471 ) -> Result<Challenge, sqlx::Error> {
472 let challenge = Challenge::new(authz_id, typ);
473 challenge.insert(&database.pool).await?;
474 Ok(challenge)
475 }
476
477 pub async fn find_by_id(
478 id: &str,
479 database: &Database,
480 ) -> Result<Option<Challenge>, sqlx::Error> {
481 debug!(event = "db_challenge_find_by_id_started", outcome = "progress", challenge_id = ?id);
482 let Some(id) = crate::sqlite::id::parse(id) else {
483 return Ok(None);
484 };
485 let row = sqlx::query(concat!(
486 "SELECT ",
487 challenge_columns!(),
488 " FROM challenges WHERE id = ?;"
489 ))
490 .bind(id)
491 .fetch_optional(&database.pool)
492 .await?;
493
494 row.map(Challenge::from_row).transpose()
495 }
496
497 pub async fn find_by_authz(
500 authz_id: Uuid,
501 database: &Database,
502 ) -> Result<Vec<Challenge>, sqlx::Error> {
503 debug!(event = "db_challenge_find_by_authz_started", outcome = "progress", authz_id = ?authz_id);
504 let rows = sqlx::query(concat!(
505 "SELECT ",
506 challenge_columns!(),
507 " FROM challenges WHERE authz_id = ? ORDER BY created_at ASC;"
508 ))
509 .bind(authz_id)
510 .fetch_all(&database.pool)
511 .await?;
512
513 rows.into_iter().map(Challenge::from_row).collect()
514 }
515
516 pub async fn claim_for_validation(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
541 debug!(event = "db_challenge_claim_started", outcome = "progress", challenge_id = ?self.id);
542 let claimed = sqlx::query(
543 "UPDATE challenges SET status = 'processing' WHERE id = ? AND status = 'pending';",
544 )
545 .bind(self.id)
546 .execute(&database.pool)
547 .await?
548 .rows_affected()
549 == 1;
550
551 if !claimed {
552 debug!(event = "db_challenge_claim_refused", outcome = "advisory", challenge_id = ?self.id);
553 return Ok(false);
554 }
555
556 self.status = ChallengeStatus::Processing;
557 debug!(event = "db_challenge_claimed", outcome = "success", challenge_id = ?self.id);
558 Ok(true)
559 }
560
561 pub(crate) async fn set_valid<'e, E>(
570 id: Uuid,
571 validated: i64,
572 executor: E,
573 ) -> Result<(), sqlx::Error>
574 where
575 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
576 {
577 sqlx::query("UPDATE challenges SET status = 'valid', validated = ? WHERE id = ?;")
578 .bind(validated)
579 .bind(id)
580 .execute(executor)
581 .await?;
582 Ok(())
583 }
584
585 pub(crate) async fn set_invalid<'e, E>(
587 id: Uuid,
588 error: &Value,
589 executor: E,
590 ) -> Result<(), sqlx::Error>
591 where
592 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
593 {
594 let error_json =
595 serde_json::to_string(error).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
596 sqlx::query("UPDATE challenges SET status = 'invalid', error = ? WHERE id = ?;")
597 .bind(error_json)
598 .bind(id)
599 .execute(executor)
600 .await?;
601 Ok(())
602 }
603
604 pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
605 let validated = now_secs();
606 debug!(event = "db_challenge_mark_valid_started", outcome = "progress", challenge_id = ?self.id);
607 Self::set_valid(self.id, validated, &database.pool).await?;
608
609 self.status = ChallengeStatus::Valid;
610 self.validated = Some(validated);
611 info!(event = "db_challenge_marked_valid", outcome = "success", challenge_id = ?self.id);
612 Ok(())
613 }
614
615 pub async fn mark_invalid(
622 &mut self,
623 error: Value,
624 database: &Database,
625 ) -> Result<(), sqlx::Error> {
626 debug!(event = "db_challenge_mark_invalid_started", outcome = "progress", challenge_id = ?self.id);
627 Self::set_invalid(self.id, &error, &database.pool).await?;
628
629 self.status = ChallengeStatus::Invalid;
630 self.error = Some(error);
631 info!(event = "db_challenge_marked_invalid", outcome = "failure", challenge_id = ?self.id);
632 Ok(())
633 }
634
635 #[must_use]
638 pub fn to_json(&self, base_url: &str) -> Value {
639 let mut object = serde_json::Map::new();
640 object.insert("type".to_string(), Value::String(self.typ.clone()));
641 object.insert(
642 "url".to_string(),
643 Value::String(format!("{base_url}/chall/{}", self.id)),
644 );
645 object.insert(
646 "status".to_string(),
647 Value::String(self.status.as_str().to_string()),
648 );
649 object.insert("token".to_string(), Value::String(self.token.clone()));
650 if let Some(validated) = self.validated {
651 object.insert("validated".to_string(), Value::String(rfc3339(validated)));
652 }
653 if let Some(error) = &self.error {
654 object.insert("error".to_string(), error.clone());
655 }
656 Value::Object(object)
657 }
658}
659
660#[cfg(test)]
661mod tests {
662
663 use super::*;
664 use crate::audit::ClientContext;
665 use crate::sqlite::account::Account;
666 use crate::sqlite::order::Order;
667 use crate::sqlite::status::OrderStatus;
668 use crate::testutil::account_id;
669 use std::sync::Arc;
670
671 #[tokio::test]
673 async fn ids_for_several_orders_come_back_grouped_in_one_query() {
674 let db = Arc::new(Database::connect_in_memory().await.unwrap());
675 let account = account_id(&db).await;
676
677 let mut expected = Vec::new();
678 for name in ["a.example.com", "b.example.com"] {
679 let order = Order::create(
680 "default",
681 account,
682 vec![Identifier::dns(name)],
683 now_secs() + 3600,
684 None,
685 None,
686 &db,
687 )
688 .await
689 .unwrap();
690 let first =
691 Authorization::create(order.id, Identifier::dns(name), now_secs() + 3600, &db)
692 .await
693 .unwrap();
694 expected.push((order.id, first.id));
695 }
696
697 let ids: Vec<Uuid> = expected.iter().map(|(o, _)| *o).collect();
698 let grouped = Authorization::find_ids_by_orders(&ids, &db).await.unwrap();
699 assert_eq!(grouped.len(), 2);
700 for (order_id, authz_id) in &expected {
701 assert_eq!(grouped[order_id], vec![*authz_id]);
702 }
703
704 let grouped = Authorization::find_ids_by_orders(&[crate::sqlite::id::mint()], &db)
707 .await
708 .unwrap();
709 assert!(grouped.is_empty());
710
711 assert!(
713 Authorization::find_ids_by_orders(&[], &db)
714 .await
715 .unwrap()
716 .is_empty()
717 );
718 }
719
720 async fn order_id(db: &Arc<Database>) -> String {
723 let (account, _) = Account::find_or_create(
724 "default",
725 &[1u8, 2, 3],
726 vec![],
727 &ClientContext::default(),
728 db,
729 )
730 .await
731 .unwrap();
732 let order = Order::create(
733 "default",
734 account.id,
735 vec![Identifier::dns("example.com")],
736 now_secs() + 3600,
737 None,
738 None,
739 db,
740 )
741 .await
742 .unwrap();
743 order.id.to_string()
744 }
745
746 #[tokio::test]
755 async fn the_validation_transitions_roll_back_together() {
756 let db = Arc::new(Database::connect_in_memory().await.unwrap());
757 let oid = order_id(&db).await;
758 let authz = Authorization::create(
759 oid.parse().unwrap(),
760 Identifier::dns("example.com"),
761 now_secs() + 3600,
762 &db,
763 )
764 .await
765 .unwrap();
766 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
767
768 let mut tx = db.pool.begin().await.unwrap();
771 Challenge::set_valid(challenge.id, now_secs(), &mut *tx)
772 .await
773 .unwrap();
774 Authorization::set_valid(authz.id, &mut *tx).await.unwrap();
775 Order::set_ready(oid.parse().unwrap(), &mut *tx)
776 .await
777 .unwrap();
778 tx.rollback().await.unwrap();
779
780 let reloaded_authz = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
781 .await
782 .unwrap()
783 .unwrap();
784 let reloaded_challenge = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
785 .await
786 .unwrap()
787 .unwrap();
788 let reloaded_order = Order::find_by_id(&oid, &db).await.unwrap().unwrap();
789
790 assert_eq!(reloaded_challenge.status, ChallengeStatus::Pending);
791 assert_eq!(reloaded_authz.status, AuthzStatus::Pending);
792 assert_eq!(reloaded_order.status, OrderStatus::Pending);
793 }
794
795 #[tokio::test]
798 async fn the_validation_transitions_commit_together() {
799 let db = Arc::new(Database::connect_in_memory().await.unwrap());
800 let oid = order_id(&db).await;
801 let authz = Authorization::create(
802 oid.parse().unwrap(),
803 Identifier::dns("example.com"),
804 now_secs() + 3600,
805 &db,
806 )
807 .await
808 .unwrap();
809 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
810
811 let mut tx = db.pool.begin().await.unwrap();
812 Challenge::set_valid(challenge.id, now_secs(), &mut *tx)
813 .await
814 .unwrap();
815 Authorization::set_valid(authz.id, &mut *tx).await.unwrap();
816 Order::set_ready(oid.parse().unwrap(), &mut *tx)
817 .await
818 .unwrap();
819 tx.commit().await.unwrap();
820
821 assert_eq!(
822 Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
823 .await
824 .unwrap()
825 .unwrap()
826 .status,
827 ChallengeStatus::Valid
828 );
829 assert_eq!(
830 Authorization::find_by_id(authz.id.to_string().as_str(), &db)
831 .await
832 .unwrap()
833 .unwrap()
834 .status,
835 AuthzStatus::Valid
836 );
837 assert_eq!(
838 Order::find_by_id(&oid, &db).await.unwrap().unwrap().status,
839 OrderStatus::Ready
840 );
841 }
842
843 #[tokio::test]
844 async fn authz_create_find_round_trip() {
845 let db = Arc::new(Database::connect_in_memory().await.unwrap());
846 let oid = order_id(&db).await;
847
848 let authz = Authorization::create(
849 oid.parse().unwrap(),
850 Identifier::dns("example.com"),
851 now_secs() + 3600,
852 &db,
853 )
854 .await
855 .unwrap();
856 assert_eq!(authz.status, AuthzStatus::Pending);
857
858 let by_id = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
859 .await
860 .unwrap()
861 .unwrap();
862 assert_eq!(by_id.identifier, Identifier::dns("example.com"));
863 assert_eq!(by_id.order_id.to_string(), oid);
864
865 let by_order = Authorization::find_by_order(oid.parse().unwrap(), &db)
866 .await
867 .unwrap();
868 assert_eq!(by_order.len(), 1);
869 }
870
871 #[tokio::test]
872 async fn authz_mark_valid_persists_and_syncs() {
873 let db = Arc::new(Database::connect_in_memory().await.unwrap());
874 let oid = order_id(&db).await;
875
876 let mut authz = Authorization::create(
877 oid.parse().unwrap(),
878 Identifier::dns("example.com"),
879 now_secs() + 3600,
880 &db,
881 )
882 .await
883 .unwrap();
884 authz.mark_valid(&db).await.unwrap();
885
886 assert_eq!(authz.status, AuthzStatus::Valid);
887 let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
888 .await
889 .unwrap()
890 .unwrap();
891 assert_eq!(reloaded.status, AuthzStatus::Valid);
892 }
893
894 #[tokio::test]
895 async fn authz_to_json_shape() {
896 let db = Arc::new(Database::connect_in_memory().await.unwrap());
897 let oid = order_id(&db).await;
898
899 let authz = Authorization::create(
900 oid.parse().unwrap(),
901 Identifier::dns("example.com"),
902 now_secs() + 3600,
903 &db,
904 )
905 .await
906 .unwrap();
907 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
908
909 let json = authz.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
910 assert_eq!(json["status"], "pending");
911 assert_eq!(
912 json["identifier"],
913 serde_json::json!({"type":"dns","value":"example.com"})
914 );
915 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
916 assert_eq!(json["challenges"].as_array().unwrap().len(), 1);
917 assert_eq!(json["challenges"][0]["type"], "http-01");
918 }
919
920 #[tokio::test]
921 async fn challenge_create_find_round_trip() {
922 let db = Arc::new(Database::connect_in_memory().await.unwrap());
923 let oid = order_id(&db).await;
924 let authz = Authorization::create(
925 oid.parse().unwrap(),
926 Identifier::dns("example.com"),
927 now_secs() + 3600,
928 &db,
929 )
930 .await
931 .unwrap();
932
933 let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
934 assert_eq!(challenge.typ, "http-01");
935 assert_eq!(challenge.status, ChallengeStatus::Pending);
936 assert!(!challenge.token.is_empty());
937 assert!(challenge.validated.is_none());
938
939 let by_id = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
940 .await
941 .unwrap()
942 .unwrap();
943 assert_eq!(by_id.token, challenge.token);
944
945 let by_authz = Challenge::find_by_authz(authz.id, &db).await.unwrap();
946 assert_eq!(by_authz.len(), 1);
947 }
948
949 #[tokio::test]
950 async fn challenge_mark_valid_persists_and_syncs() {
951 let db = Arc::new(Database::connect_in_memory().await.unwrap());
952 let oid = order_id(&db).await;
953 let authz = Authorization::create(
954 oid.parse().unwrap(),
955 Identifier::dns("example.com"),
956 now_secs() + 3600,
957 &db,
958 )
959 .await
960 .unwrap();
961
962 let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
963 challenge.mark_valid(&db).await.unwrap();
964
965 assert_eq!(challenge.status, ChallengeStatus::Valid);
966 assert!(challenge.validated.is_some());
967
968 let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
969 .await
970 .unwrap()
971 .unwrap();
972 assert_eq!(reloaded.status, ChallengeStatus::Valid);
973 let json = reloaded.to_json("http://localhost:3000");
974 assert_eq!(json["status"], "valid");
975 assert_eq!(
976 json["url"],
977 format!("http://localhost:3000/chall/{}", challenge.id)
978 );
979 assert!(json["validated"].as_str().unwrap().ends_with('Z'));
980 }
981
982 #[tokio::test]
983 async fn challenge_mark_invalid_persists_the_problem_document() {
984 let db = Arc::new(Database::connect_in_memory().await.unwrap());
985 let oid = order_id(&db).await;
986 let authz = Authorization::create(
987 oid.parse().unwrap(),
988 Identifier::dns("example.com"),
989 now_secs() + 3600,
990 &db,
991 )
992 .await
993 .unwrap();
994
995 let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
996 let problem = serde_json::json!({
997 "type": "urn:ietf:params:acme:error:incorrectResponse",
998 "detail": "response body does not match the key authorization",
999 "status": 403,
1000 });
1001 challenge.mark_invalid(problem.clone(), &db).await.unwrap();
1002
1003 assert_eq!(challenge.status, ChallengeStatus::Invalid);
1004 assert_eq!(challenge.error.as_ref(), Some(&problem));
1005 assert!(challenge.validated.is_none());
1007
1008 let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
1009 .await
1010 .unwrap()
1011 .unwrap();
1012 assert_eq!(reloaded.status, ChallengeStatus::Invalid);
1013 assert_eq!(reloaded.error.as_ref(), Some(&problem));
1014 let json = reloaded.to_json("http://localhost:3000");
1015 assert_eq!(json["error"], problem);
1016 assert!(json.get("validated").is_none());
1017 }
1018
1019 #[tokio::test]
1020 async fn authz_mark_invalid_persists_and_syncs() {
1021 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1022 let oid = order_id(&db).await;
1023
1024 let mut authz = Authorization::create(
1025 oid.parse().unwrap(),
1026 Identifier::dns("example.com"),
1027 now_secs() + 3600,
1028 &db,
1029 )
1030 .await
1031 .unwrap();
1032 authz.mark_invalid(&db).await.unwrap();
1033
1034 assert_eq!(authz.status, AuthzStatus::Invalid);
1035 let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1036 .await
1037 .unwrap()
1038 .unwrap();
1039 assert_eq!(reloaded.status, AuthzStatus::Invalid);
1040 }
1041
1042 #[tokio::test]
1045 async fn an_authorization_holds_one_challenge_per_type() {
1046 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1047 let oid = order_id(&db).await;
1048 let authz = Authorization::create(
1049 oid.parse().unwrap(),
1050 Identifier::dns("example.com"),
1051 now_secs() + 3600,
1052 &db,
1053 )
1054 .await
1055 .unwrap();
1056
1057 let http = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1058 let dns_01 = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
1059 Challenge::create(authz.id, "tls-alpn-01", &db)
1060 .await
1061 .unwrap();
1062
1063 assert_ne!(http.token, dns_01.token);
1065
1066 let challenges = Challenge::find_by_authz(authz.id, &db).await.unwrap();
1067 assert_eq!(challenges.len(), 3);
1068
1069 assert!(Challenge::create(authz.id, "http-01", &db).await.is_err());
1071 }
1072
1073 #[tokio::test]
1077 async fn a_wildcard_authorization_stores_the_prefix_and_renders_the_base_name() {
1078 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1079 let oid = order_id(&db).await;
1080
1081 let authz = Authorization::create(
1082 oid.parse().unwrap(),
1083 Identifier::dns("*.example.com"),
1084 now_secs() + 3600,
1085 &db,
1086 )
1087 .await
1088 .unwrap();
1089 let challenge = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
1090
1091 let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1094 .await
1095 .unwrap()
1096 .unwrap();
1097 assert_eq!(reloaded.identifier.value, "*.example.com");
1098 assert!(reloaded.is_wildcard());
1099 assert_eq!(reloaded.base_identifier(), "example.com");
1100
1101 let json = reloaded.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
1102 assert_eq!(
1103 json["identifier"],
1104 serde_json::json!({"type":"dns","value":"example.com"})
1105 );
1106 assert_eq!(json["wildcard"], true);
1107 assert_eq!(json["challenges"][0]["type"], "dns-01");
1108 }
1109
1110 #[tokio::test]
1113 async fn a_plain_authorization_has_no_wildcard_member() {
1114 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1115 let oid = order_id(&db).await;
1116
1117 let authz = Authorization::create(
1118 oid.parse().unwrap(),
1119 Identifier::dns("example.com"),
1120 now_secs() + 3600,
1121 &db,
1122 )
1123 .await
1124 .unwrap();
1125 assert!(!authz.is_wildcard());
1126 assert_eq!(authz.base_identifier(), "example.com");
1127 assert!(
1128 authz
1129 .to_json("http://localhost:3000", &[])
1130 .get("wildcard")
1131 .is_none()
1132 );
1133 }
1134
1135 #[tokio::test]
1136 async fn absent_lookups_return_none() {
1137 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1138 assert!(
1139 Authorization::find_by_id("nope", &db)
1140 .await
1141 .unwrap()
1142 .is_none()
1143 );
1144 assert!(Challenge::find_by_id("nope", &db).await.unwrap().is_none());
1145 }
1146}