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: String,
39 pub order_id: String,
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: String,
60 pub authz_id: String,
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: &str, identifier: Identifier, expires: i64) -> Authorization {
101 Authorization {
102 id: Uuid::new_v4().to_string(),
103 order_id: order_id.to_string(),
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: &str,
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 row = sqlx::query(concat!(
157 "SELECT ",
158 authz_columns!(),
159 " FROM authorizations WHERE id = ?;"
160 ))
161 .bind(id)
162 .fetch_optional(&database.pool)
163 .await?;
164
165 row.map(Authorization::from_row).transpose()
166 }
167
168 pub async fn find_by_order(
171 order_id: &str,
172 database: &Database,
173 ) -> Result<Vec<Authorization>, sqlx::Error> {
174 Self::find_by_order_with(order_id, &database.pool).await
175 }
176
177 pub async fn count_by_order(order_id: &str, database: &Database) -> Result<i64, sqlx::Error> {
180 let row = sqlx::query("SELECT COUNT(*) FROM authorizations WHERE order_id = ?;")
181 .bind(order_id)
182 .fetch_one(&database.pool)
183 .await?;
184 row.try_get::<i64, _>(0)
185 }
186
187 pub async fn find_ids_by_orders(
197 order_ids: &[&str],
198 database: &Database,
199 ) -> Result<std::collections::HashMap<String, Vec<String>>, sqlx::Error> {
200 let mut grouped: std::collections::HashMap<String, Vec<String>> =
201 std::collections::HashMap::new();
202 if order_ids.is_empty() {
203 return Ok(grouped);
204 }
205
206 let mut builder =
210 sqlx::QueryBuilder::new("SELECT id, order_id FROM authorizations WHERE order_id IN (");
211 let mut separated = builder.separated(", ");
212 for id in order_ids {
213 separated.push_bind(*id);
214 }
215 builder.push(") ORDER BY created_at ASC;");
216
217 debug!(
218 event = "db_authz_find_ids_by_orders",
219 outcome = "success",
220 orders = order_ids.len()
221 );
222 for row in builder.build().fetch_all(&database.pool).await? {
223 let order_id: String = row.try_get("order_id")?;
224 let id: String = row.try_get("id")?;
225 grouped.entry(order_id).or_default().push(id);
226 }
227 Ok(grouped)
228 }
229
230 pub(crate) async fn find_by_order_with<'e, E>(
239 order_id: &str,
240 executor: E,
241 ) -> Result<Vec<Authorization>, sqlx::Error>
242 where
243 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
244 {
245 debug!(event = "db_authz_find_by_order_started", outcome = "progress", order_id = ?order_id);
246 let rows = sqlx::query(concat!(
247 "SELECT ",
248 authz_columns!(),
249 " FROM authorizations WHERE order_id = ? ORDER BY created_at ASC;"
250 ))
251 .bind(order_id)
252 .fetch_all(executor)
253 .await?;
254
255 rows.into_iter().map(Authorization::from_row).collect()
256 }
257
258 pub(crate) async fn set_valid<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
266 where
267 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
268 {
269 sqlx::query("UPDATE authorizations SET status = 'valid' WHERE id = ?;")
270 .bind(id)
271 .execute(executor)
272 .await?;
273 Ok(())
274 }
275
276 pub(crate) async fn set_invalid<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
278 where
279 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
280 {
281 sqlx::query("UPDATE authorizations SET status = 'invalid' WHERE id = ?;")
282 .bind(id)
283 .execute(executor)
284 .await?;
285 Ok(())
286 }
287
288 pub(crate) async fn set_deactivated<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
299 where
300 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
301 {
302 sqlx::query("UPDATE authorizations SET status = 'deactivated' WHERE id = ?;")
303 .bind(id)
304 .execute(executor)
305 .await?;
306 Ok(())
307 }
308
309 pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
312 debug!(event = "db_authz_mark_valid_started", outcome = "progress", authz_id = ?self.id);
313 Self::set_valid(&self.id, &database.pool).await?;
314
315 self.status = AuthzStatus::Valid;
316 info!(event = "db_authz_marked_valid", outcome = "success", authz_id = ?self.id);
317 Ok(())
318 }
319
320 pub async fn mark_invalid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
327 debug!(event = "db_authz_mark_invalid_started", outcome = "progress", authz_id = ?self.id);
328 Self::set_invalid(&self.id, &database.pool).await?;
329
330 self.status = AuthzStatus::Invalid;
331 info!(event = "db_authz_marked_invalid", outcome = "failure", authz_id = ?self.id);
332 Ok(())
333 }
334
335 #[must_use]
340 pub fn is_wildcard(&self) -> bool {
341 self.identifier.value.starts_with("*.")
342 }
343
344 #[must_use]
350 pub fn base_identifier(&self) -> &str {
351 self.identifier
352 .value
353 .strip_prefix("*.")
354 .unwrap_or(&self.identifier.value)
355 }
356
357 #[must_use]
361 pub fn to_json(&self, base_url: &str, challenges: &[Challenge]) -> Value {
362 let mut object = serde_json::Map::new();
363 object.insert(
367 "identifier".to_string(),
368 serde_json::to_value(Identifier::new(
369 self.identifier.typ.clone(),
370 self.base_identifier().to_string(),
371 ))
372 .expect("Identifier is always serializable"),
373 );
374 object.insert(
375 "status".to_string(),
376 Value::String(self.status.as_str().to_string()),
377 );
378 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
379 let challenges: Vec<Value> = challenges.iter().map(|c| c.to_json(base_url)).collect();
380 object.insert("challenges".to_string(), Value::Array(challenges));
381 if self.is_wildcard() {
382 object.insert("wildcard".to_string(), Value::Bool(true));
383 }
384 Value::Object(object)
385 }
386}
387
388macro_rules! challenge_columns {
394 () => {
395 "id, authz_id, type, token, status, validated, error, created_at"
396 };
397}
398
399impl Challenge {
400 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
401 let error_json: Option<String> = row.try_get("error")?;
402 let error = error_json
403 .map(|json| serde_json::from_str(&json))
404 .transpose()
405 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
406
407 Ok(Challenge {
408 id: row.try_get("id")?,
409 authz_id: row.try_get("authz_id")?,
410 typ: row.try_get("type")?,
411 token: row.try_get("token")?,
412 status: status::from_column(row.try_get::<&str, _>("status")?)?,
413 validated: row.try_get("validated")?,
414 error,
415 created_at: row.try_get("created_at")?,
416 })
417 }
418
419 pub(crate) fn new(authz_id: &str, typ: &str) -> Challenge {
426 Challenge {
427 id: Uuid::new_v4().to_string(),
428 authz_id: authz_id.to_string(),
429 typ: typ.to_string(),
430 token: random_token(),
431 status: ChallengeStatus::Pending,
432 validated: None,
433 error: None,
434 created_at: now_secs(),
435 }
436 }
437
438 pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
441 where
442 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
443 {
444 debug!(event = "db_challenge_create_started", outcome = "progress", challenge_id = ?self.id, authz_id = ?self.authz_id);
445 sqlx::query(
446 "INSERT INTO challenges (id, authz_id, type, token, status, validated, created_at) \
447 VALUES (?, ?, ?, ?, ?, NULL, ?);",
448 )
449 .bind(&self.id)
450 .bind(&self.authz_id)
451 .bind(&self.typ)
452 .bind(&self.token)
453 .bind(self.status.as_str())
454 .bind(self.created_at)
455 .execute(executor)
456 .await?;
457
458 info!(event = "db_challenge_created", outcome = "success", challenge_id = ?self.id, authz_id = ?self.authz_id);
459 Ok(())
460 }
461
462 pub async fn create(
465 authz_id: &str,
466 typ: &str,
467 database: &Database,
468 ) -> Result<Challenge, sqlx::Error> {
469 let challenge = Challenge::new(authz_id, typ);
470 challenge.insert(&database.pool).await?;
471 Ok(challenge)
472 }
473
474 pub async fn find_by_id(
475 id: &str,
476 database: &Database,
477 ) -> Result<Option<Challenge>, sqlx::Error> {
478 debug!(event = "db_challenge_find_by_id_started", outcome = "progress", challenge_id = ?id);
479 let row = sqlx::query(concat!(
480 "SELECT ",
481 challenge_columns!(),
482 " FROM challenges WHERE id = ?;"
483 ))
484 .bind(id)
485 .fetch_optional(&database.pool)
486 .await?;
487
488 row.map(Challenge::from_row).transpose()
489 }
490
491 pub async fn find_by_authz(
494 authz_id: &str,
495 database: &Database,
496 ) -> Result<Vec<Challenge>, sqlx::Error> {
497 debug!(event = "db_challenge_find_by_authz_started", outcome = "progress", authz_id = ?authz_id);
498 let rows = sqlx::query(concat!(
499 "SELECT ",
500 challenge_columns!(),
501 " FROM challenges WHERE authz_id = ? ORDER BY created_at ASC;"
502 ))
503 .bind(authz_id)
504 .fetch_all(&database.pool)
505 .await?;
506
507 rows.into_iter().map(Challenge::from_row).collect()
508 }
509
510 pub async fn claim_for_validation(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
535 debug!(event = "db_challenge_claim_started", outcome = "progress", challenge_id = ?self.id);
536 let claimed = sqlx::query(
537 "UPDATE challenges SET status = 'processing' WHERE id = ? AND status = 'pending';",
538 )
539 .bind(&self.id)
540 .execute(&database.pool)
541 .await?
542 .rows_affected()
543 == 1;
544
545 if !claimed {
546 debug!(event = "db_challenge_claim_refused", outcome = "advisory", challenge_id = ?self.id);
547 return Ok(false);
548 }
549
550 self.status = ChallengeStatus::Processing;
551 debug!(event = "db_challenge_claimed", outcome = "success", challenge_id = ?self.id);
552 Ok(true)
553 }
554
555 pub(crate) async fn set_valid<'e, E>(
564 id: &str,
565 validated: i64,
566 executor: E,
567 ) -> Result<(), sqlx::Error>
568 where
569 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
570 {
571 sqlx::query("UPDATE challenges SET status = 'valid', validated = ? WHERE id = ?;")
572 .bind(validated)
573 .bind(id)
574 .execute(executor)
575 .await?;
576 Ok(())
577 }
578
579 pub(crate) async fn set_invalid<'e, E>(
581 id: &str,
582 error: &Value,
583 executor: E,
584 ) -> Result<(), sqlx::Error>
585 where
586 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
587 {
588 let error_json =
589 serde_json::to_string(error).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
590 sqlx::query("UPDATE challenges SET status = 'invalid', error = ? WHERE id = ?;")
591 .bind(error_json)
592 .bind(id)
593 .execute(executor)
594 .await?;
595 Ok(())
596 }
597
598 pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
599 let validated = now_secs();
600 debug!(event = "db_challenge_mark_valid_started", outcome = "progress", challenge_id = ?self.id);
601 Self::set_valid(&self.id, validated, &database.pool).await?;
602
603 self.status = ChallengeStatus::Valid;
604 self.validated = Some(validated);
605 info!(event = "db_challenge_marked_valid", outcome = "success", challenge_id = ?self.id);
606 Ok(())
607 }
608
609 pub async fn mark_invalid(
616 &mut self,
617 error: Value,
618 database: &Database,
619 ) -> Result<(), sqlx::Error> {
620 debug!(event = "db_challenge_mark_invalid_started", outcome = "progress", challenge_id = ?self.id);
621 Self::set_invalid(&self.id, &error, &database.pool).await?;
622
623 self.status = ChallengeStatus::Invalid;
624 self.error = Some(error);
625 info!(event = "db_challenge_marked_invalid", outcome = "failure", challenge_id = ?self.id);
626 Ok(())
627 }
628
629 #[must_use]
632 pub fn to_json(&self, base_url: &str) -> Value {
633 let mut object = serde_json::Map::new();
634 object.insert("type".to_string(), Value::String(self.typ.clone()));
635 object.insert(
636 "url".to_string(),
637 Value::String(format!("{base_url}/chall/{}", self.id)),
638 );
639 object.insert(
640 "status".to_string(),
641 Value::String(self.status.as_str().to_string()),
642 );
643 object.insert("token".to_string(), Value::String(self.token.clone()));
644 if let Some(validated) = self.validated {
645 object.insert("validated".to_string(), Value::String(rfc3339(validated)));
646 }
647 if let Some(error) = &self.error {
648 object.insert("error".to_string(), error.clone());
649 }
650 Value::Object(object)
651 }
652}
653
654#[cfg(test)]
655mod tests {
656
657 use super::*;
658 use crate::audit::ClientContext;
659 use crate::sqlite::account::Account;
660 use crate::sqlite::order::Order;
661 use crate::sqlite::status::OrderStatus;
662 use crate::testutil::account_id;
663 use std::sync::Arc;
664
665 #[tokio::test]
667 async fn ids_for_several_orders_come_back_grouped_in_one_query() {
668 let db = Arc::new(Database::connect_in_memory().await.unwrap());
669 let account = account_id(&db).await;
670
671 let mut expected = Vec::new();
672 for name in ["a.example.com", "b.example.com"] {
673 let order = Order::create(
674 "default",
675 &account,
676 vec![Identifier::dns(name)],
677 now_secs() + 3600,
678 None,
679 None,
680 &db,
681 )
682 .await
683 .unwrap();
684 let first =
685 Authorization::create(&order.id, Identifier::dns(name), now_secs() + 3600, &db)
686 .await
687 .unwrap();
688 expected.push((order.id, first.id));
689 }
690
691 let ids: Vec<&str> = expected.iter().map(|(o, _)| o.as_str()).collect();
692 let grouped = Authorization::find_ids_by_orders(&ids, &db).await.unwrap();
693 assert_eq!(grouped.len(), 2);
694 for (order_id, authz_id) in &expected {
695 assert_eq!(grouped[order_id], vec![authz_id.clone()]);
696 }
697
698 let grouped = Authorization::find_ids_by_orders(&["nope"], &db)
701 .await
702 .unwrap();
703 assert!(grouped.is_empty());
704
705 assert!(
707 Authorization::find_ids_by_orders(&[], &db)
708 .await
709 .unwrap()
710 .is_empty()
711 );
712 }
713
714 async fn order_id(db: &Arc<Database>) -> String {
717 let (account, _) = Account::find_or_create(
718 "default",
719 &[1u8, 2, 3],
720 vec![],
721 &ClientContext::default(),
722 db,
723 )
724 .await
725 .unwrap();
726 let order = Order::create(
727 "default",
728 &account.id,
729 vec![Identifier::dns("example.com")],
730 now_secs() + 3600,
731 None,
732 None,
733 db,
734 )
735 .await
736 .unwrap();
737 order.id
738 }
739
740 #[tokio::test]
749 async fn the_validation_transitions_roll_back_together() {
750 let db = Arc::new(Database::connect_in_memory().await.unwrap());
751 let oid = order_id(&db).await;
752 let authz =
753 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
754 .await
755 .unwrap();
756 let challenge = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
757
758 let mut tx = db.pool.begin().await.unwrap();
761 Challenge::set_valid(&challenge.id, now_secs(), &mut *tx)
762 .await
763 .unwrap();
764 Authorization::set_valid(&authz.id, &mut *tx).await.unwrap();
765 Order::set_ready(&oid, &mut *tx).await.unwrap();
766 tx.rollback().await.unwrap();
767
768 let reloaded_authz = Authorization::find_by_id(&authz.id, &db)
769 .await
770 .unwrap()
771 .unwrap();
772 let reloaded_challenge = Challenge::find_by_id(&challenge.id, &db)
773 .await
774 .unwrap()
775 .unwrap();
776 let reloaded_order = Order::find_by_id(&oid, &db).await.unwrap().unwrap();
777
778 assert_eq!(reloaded_challenge.status, ChallengeStatus::Pending);
779 assert_eq!(reloaded_authz.status, AuthzStatus::Pending);
780 assert_eq!(reloaded_order.status, OrderStatus::Pending);
781 }
782
783 #[tokio::test]
786 async fn the_validation_transitions_commit_together() {
787 let db = Arc::new(Database::connect_in_memory().await.unwrap());
788 let oid = order_id(&db).await;
789 let authz =
790 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
791 .await
792 .unwrap();
793 let challenge = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
794
795 let mut tx = db.pool.begin().await.unwrap();
796 Challenge::set_valid(&challenge.id, now_secs(), &mut *tx)
797 .await
798 .unwrap();
799 Authorization::set_valid(&authz.id, &mut *tx).await.unwrap();
800 Order::set_ready(&oid, &mut *tx).await.unwrap();
801 tx.commit().await.unwrap();
802
803 assert_eq!(
804 Challenge::find_by_id(&challenge.id, &db)
805 .await
806 .unwrap()
807 .unwrap()
808 .status,
809 ChallengeStatus::Valid
810 );
811 assert_eq!(
812 Authorization::find_by_id(&authz.id, &db)
813 .await
814 .unwrap()
815 .unwrap()
816 .status,
817 AuthzStatus::Valid
818 );
819 assert_eq!(
820 Order::find_by_id(&oid, &db).await.unwrap().unwrap().status,
821 OrderStatus::Ready
822 );
823 }
824
825 #[tokio::test]
826 async fn authz_create_find_round_trip() {
827 let db = Arc::new(Database::connect_in_memory().await.unwrap());
828 let oid = order_id(&db).await;
829
830 let authz =
831 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
832 .await
833 .unwrap();
834 assert_eq!(authz.status, AuthzStatus::Pending);
835
836 let by_id = Authorization::find_by_id(&authz.id, &db)
837 .await
838 .unwrap()
839 .unwrap();
840 assert_eq!(by_id.identifier, Identifier::dns("example.com"));
841 assert_eq!(by_id.order_id, oid);
842
843 let by_order = Authorization::find_by_order(&oid, &db).await.unwrap();
844 assert_eq!(by_order.len(), 1);
845 }
846
847 #[tokio::test]
848 async fn authz_mark_valid_persists_and_syncs() {
849 let db = Arc::new(Database::connect_in_memory().await.unwrap());
850 let oid = order_id(&db).await;
851
852 let mut authz =
853 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
854 .await
855 .unwrap();
856 authz.mark_valid(&db).await.unwrap();
857
858 assert_eq!(authz.status, AuthzStatus::Valid);
859 let reloaded = Authorization::find_by_id(&authz.id, &db)
860 .await
861 .unwrap()
862 .unwrap();
863 assert_eq!(reloaded.status, AuthzStatus::Valid);
864 }
865
866 #[tokio::test]
867 async fn authz_to_json_shape() {
868 let db = Arc::new(Database::connect_in_memory().await.unwrap());
869 let oid = order_id(&db).await;
870
871 let authz =
872 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
873 .await
874 .unwrap();
875 let challenge = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
876
877 let json = authz.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
878 assert_eq!(json["status"], "pending");
879 assert_eq!(
880 json["identifier"],
881 serde_json::json!({"type":"dns","value":"example.com"})
882 );
883 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
884 assert_eq!(json["challenges"].as_array().unwrap().len(), 1);
885 assert_eq!(json["challenges"][0]["type"], "http-01");
886 }
887
888 #[tokio::test]
889 async fn challenge_create_find_round_trip() {
890 let db = Arc::new(Database::connect_in_memory().await.unwrap());
891 let oid = order_id(&db).await;
892 let authz =
893 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
894 .await
895 .unwrap();
896
897 let challenge = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
898 assert_eq!(challenge.typ, "http-01");
899 assert_eq!(challenge.status, ChallengeStatus::Pending);
900 assert!(!challenge.token.is_empty());
901 assert!(challenge.validated.is_none());
902
903 let by_id = Challenge::find_by_id(&challenge.id, &db)
904 .await
905 .unwrap()
906 .unwrap();
907 assert_eq!(by_id.token, challenge.token);
908
909 let by_authz = Challenge::find_by_authz(&authz.id, &db).await.unwrap();
910 assert_eq!(by_authz.len(), 1);
911 }
912
913 #[tokio::test]
914 async fn challenge_mark_valid_persists_and_syncs() {
915 let db = Arc::new(Database::connect_in_memory().await.unwrap());
916 let oid = order_id(&db).await;
917 let authz =
918 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
919 .await
920 .unwrap();
921
922 let mut challenge = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
923 challenge.mark_valid(&db).await.unwrap();
924
925 assert_eq!(challenge.status, ChallengeStatus::Valid);
926 assert!(challenge.validated.is_some());
927
928 let reloaded = Challenge::find_by_id(&challenge.id, &db)
929 .await
930 .unwrap()
931 .unwrap();
932 assert_eq!(reloaded.status, ChallengeStatus::Valid);
933 let json = reloaded.to_json("http://localhost:3000");
934 assert_eq!(json["status"], "valid");
935 assert_eq!(
936 json["url"],
937 format!("http://localhost:3000/chall/{}", challenge.id)
938 );
939 assert!(json["validated"].as_str().unwrap().ends_with('Z'));
940 }
941
942 #[tokio::test]
943 async fn challenge_mark_invalid_persists_the_problem_document() {
944 let db = Arc::new(Database::connect_in_memory().await.unwrap());
945 let oid = order_id(&db).await;
946 let authz =
947 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
948 .await
949 .unwrap();
950
951 let mut challenge = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
952 let problem = serde_json::json!({
953 "type": "urn:ietf:params:acme:error:incorrectResponse",
954 "detail": "response body does not match the key authorization",
955 "status": 403,
956 });
957 challenge.mark_invalid(problem.clone(), &db).await.unwrap();
958
959 assert_eq!(challenge.status, ChallengeStatus::Invalid);
960 assert_eq!(challenge.error.as_ref(), Some(&problem));
961 assert!(challenge.validated.is_none());
963
964 let reloaded = Challenge::find_by_id(&challenge.id, &db)
965 .await
966 .unwrap()
967 .unwrap();
968 assert_eq!(reloaded.status, ChallengeStatus::Invalid);
969 assert_eq!(reloaded.error.as_ref(), Some(&problem));
970 let json = reloaded.to_json("http://localhost:3000");
971 assert_eq!(json["error"], problem);
972 assert!(json.get("validated").is_none());
973 }
974
975 #[tokio::test]
976 async fn authz_mark_invalid_persists_and_syncs() {
977 let db = Arc::new(Database::connect_in_memory().await.unwrap());
978 let oid = order_id(&db).await;
979
980 let mut authz =
981 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
982 .await
983 .unwrap();
984 authz.mark_invalid(&db).await.unwrap();
985
986 assert_eq!(authz.status, AuthzStatus::Invalid);
987 let reloaded = Authorization::find_by_id(&authz.id, &db)
988 .await
989 .unwrap()
990 .unwrap();
991 assert_eq!(reloaded.status, AuthzStatus::Invalid);
992 }
993
994 #[tokio::test]
997 async fn an_authorization_holds_one_challenge_per_type() {
998 let db = Arc::new(Database::connect_in_memory().await.unwrap());
999 let oid = order_id(&db).await;
1000 let authz =
1001 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
1002 .await
1003 .unwrap();
1004
1005 let http = Challenge::create(&authz.id, "http-01", &db).await.unwrap();
1006 let dns_01 = Challenge::create(&authz.id, "dns-01", &db).await.unwrap();
1007 Challenge::create(&authz.id, "tls-alpn-01", &db)
1008 .await
1009 .unwrap();
1010
1011 assert_ne!(http.token, dns_01.token);
1013
1014 let challenges = Challenge::find_by_authz(&authz.id, &db).await.unwrap();
1015 assert_eq!(challenges.len(), 3);
1016
1017 assert!(Challenge::create(&authz.id, "http-01", &db).await.is_err());
1019 }
1020
1021 #[tokio::test]
1025 async fn a_wildcard_authorization_stores_the_prefix_and_renders_the_base_name() {
1026 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1027 let oid = order_id(&db).await;
1028
1029 let authz = Authorization::create(
1030 &oid,
1031 Identifier::dns("*.example.com"),
1032 now_secs() + 3600,
1033 &db,
1034 )
1035 .await
1036 .unwrap();
1037 let challenge = Challenge::create(&authz.id, "dns-01", &db).await.unwrap();
1038
1039 let reloaded = Authorization::find_by_id(&authz.id, &db)
1042 .await
1043 .unwrap()
1044 .unwrap();
1045 assert_eq!(reloaded.identifier.value, "*.example.com");
1046 assert!(reloaded.is_wildcard());
1047 assert_eq!(reloaded.base_identifier(), "example.com");
1048
1049 let json = reloaded.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
1050 assert_eq!(
1051 json["identifier"],
1052 serde_json::json!({"type":"dns","value":"example.com"})
1053 );
1054 assert_eq!(json["wildcard"], true);
1055 assert_eq!(json["challenges"][0]["type"], "dns-01");
1056 }
1057
1058 #[tokio::test]
1061 async fn a_plain_authorization_has_no_wildcard_member() {
1062 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1063 let oid = order_id(&db).await;
1064
1065 let authz =
1066 Authorization::create(&oid, Identifier::dns("example.com"), now_secs() + 3600, &db)
1067 .await
1068 .unwrap();
1069 assert!(!authz.is_wildcard());
1070 assert_eq!(authz.base_identifier(), "example.com");
1071 assert!(
1072 authz
1073 .to_json("http://localhost:3000", &[])
1074 .get("wildcard")
1075 .is_none()
1076 );
1077 }
1078
1079 #[tokio::test]
1080 async fn absent_lookups_return_none() {
1081 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1082 assert!(
1083 Authorization::find_by_id("nope", &db)
1084 .await
1085 .unwrap()
1086 .is_none()
1087 );
1088 assert!(Challenge::find_by_id("nope", &db).await.unwrap().is_none());
1089 }
1090}