1use crate::sql::Row;
36use acme_proxy_core::datetime::rfc3339;
37use serde_json::Value;
38use tracing::{debug, info};
39use uuid::Uuid;
40
41use crate::db::Database;
42use crate::nonce::now_secs;
43use crate::status::{self, OrderStatus};
44use acme_proxy_core::identifier::Identifier;
45
46#[derive(Debug)]
77pub struct Order {
78 pub id: Uuid,
79 pub profile: String,
84 pub account_id: Uuid,
85 pub status: OrderStatus,
86 pub identifiers: Vec<Identifier>,
87 pub expires: i64,
88 pub not_before: Option<i64>,
89 pub not_after: Option<i64>,
90 pub error: Option<Value>,
91 pub certificate: Option<String>,
92 pub replaces: Option<String>,
98 pub cert_serial: Option<String>,
99 pub cert_pubkey: Option<Vec<u8>>,
100 pub cert_not_after: Option<i64>,
104 pub revoked_at: Option<i64>,
105 pub revocation_reason: Option<i64>,
106 pub created_at: i64,
107 pub created_ip: Option<String>,
114 pub created_ptr: Option<String>,
115}
116
117#[derive(Debug, Clone, Default)]
123pub struct OrderQuery {
124 pub profile: Option<String>,
125 pub account_id: Option<String>,
126 pub status: Option<OrderStatus>,
127 pub identifier: Option<String>,
131 pub identifier_contains: Option<String>,
136 pub cert_serial: Option<String>,
139 pub limit: i64,
142 pub offset: i64,
143}
144
145impl OrderQuery {
146 fn push_predicates(&self, builder: &mut crate::sql::Builder) {
152 let mut separator = crate::query::push_equalities(
157 builder,
158 crate::query::WHERE,
159 &[
160 ("profile = ", self.profile.as_deref()),
161 ("status = ", self.status.map(OrderStatus::as_str)),
162 ],
163 );
164
165 if let Some(account_id) = self.account_id.as_deref() {
175 builder.push(separator).push("account_id = ");
176 builder.push_bind(super::id::parse(account_id));
177 separator = " AND ";
178 }
179
180 if let Some(identifier) = self.identifier.as_deref() {
193 let source = builder.dialect().json_array_source("orders.identifiers");
194 let member = builder.dialect().json_member("value");
195 builder.push(separator).push(format!(
196 "EXISTS (SELECT 1 FROM {source} WHERE lower({member}) = "
197 ));
198 builder.push_bind(identifier.to_lowercase());
199 builder.push(")");
200 separator = " AND ";
201 }
202
203 if let Some(fragment) = self.identifier_contains.as_deref() {
207 let source = builder.dialect().json_array_source("orders.identifiers");
208 let member = builder.dialect().json_member("value");
209 let position = builder.dialect().substring_position();
210 builder.push(separator).push(format!(
211 "EXISTS (SELECT 1 FROM {source} WHERE {position}(lower({member}), "
212 ));
213 builder.push_bind(fragment.to_lowercase());
214 builder.push(") > 0)");
215 separator = " AND ";
216 }
217
218 if let Some(cert_serial) = self.cert_serial.as_deref() {
223 builder.push(separator).push("cert_serial = ");
224 builder.push_bind(cert_serial.to_string());
225 separator = " AND ";
226 }
227
228 let _ = separator;
232 }
233}
234
235macro_rules! columns {
245 () => {
246 "id, profile, account_id, status, identifiers, expires, not_before, not_after, \
247 error, certificate, replaces, cert_serial, cert_pubkey, cert_not_after, \
248 revoked_at, revocation_reason, created_at, created_ip, created_ptr"
249 };
250}
251
252macro_rules! live_certificate {
277 () => {
278 "(certificate IS NOT NULL AND revoked_at IS NULL \
279 AND (cert_not_after IS NULL OR cert_not_after < 0 OR cert_not_after > ?))"
280 };
281}
282pub(crate) use live_certificate;
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub enum GuardedDelete {
287 NotFound,
289 LiveCertificates(u64),
292 Deleted,
293}
294
295pub const UNPARSABLE_NOT_AFTER: i64 = -1;
304
305fn push_expiring_predicates(profile: Option<&str>, before: i64, builder: &mut crate::sql::Builder) {
316 builder.push(" FROM orders WHERE certificate IS NOT NULL AND revoked_at IS NULL");
317 builder.push(" AND cert_not_after >= 0 AND cert_not_after <= ");
318 builder.push_bind(before);
319 if let Some(profile) = profile {
320 builder.push(" AND profile = ");
321 builder.push_bind(profile.to_string());
322 }
323}
324
325impl Order {
326 fn from_row(row: Row) -> Result<Self, sqlx::Error> {
327 let identifiers_json: String = row.try_get("identifiers")?;
328 let identifiers: Vec<Identifier> = serde_json::from_str(&identifiers_json)
329 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
330
331 let error_json: Option<String> = row.try_get("error")?;
332 let error: Option<Value> = match error_json {
333 Some(text) => {
334 Some(serde_json::from_str(&text).map_err(|e| sqlx::Error::Decode(Box::new(e)))?)
335 }
336 None => None,
337 };
338
339 Ok(Order {
340 id: row.try_get("id")?,
341 profile: row.try_get("profile")?,
342 account_id: row.try_get("account_id")?,
343 status: status::from_column(row.try_get::<String>("status")?.as_str())?,
344 identifiers,
345 expires: row.try_get("expires")?,
346 not_before: row.try_get("not_before")?,
347 not_after: row.try_get("not_after")?,
348 error,
349 certificate: row.try_get("certificate")?,
350 replaces: row.try_get("replaces")?,
351 cert_serial: row.try_get("cert_serial")?,
352 cert_pubkey: row.try_get("cert_pubkey")?,
353 cert_not_after: row.try_get("cert_not_after")?,
354 revoked_at: row.try_get("revoked_at")?,
355 revocation_reason: row.try_get("revocation_reason")?,
356 created_at: row.try_get("created_at")?,
357 created_ip: row.try_get("created_ip")?,
358 created_ptr: row.try_get("created_ptr")?,
359 })
360 }
361
362 pub fn new(
365 profile: &str,
366 account_id: Uuid,
367 identifiers: Vec<Identifier>,
368 expires: i64,
369 not_before: Option<i64>,
370 not_after: Option<i64>,
371 ) -> Order {
372 Order {
373 id: crate::id::mint(),
374 profile: profile.to_string(),
375 account_id,
376 status: OrderStatus::Pending,
377 identifiers,
378 expires,
379 not_before,
380 not_after,
381 error: None,
382 certificate: None,
383 replaces: None,
386 cert_serial: None,
387 cert_pubkey: None,
388 cert_not_after: None,
389 revoked_at: None,
390 revocation_reason: None,
391 created_at: now_secs(),
392 created_ip: None,
398 created_ptr: None,
399 }
400 }
401
402 #[must_use]
409 pub fn with_client(mut self, client: &acme_proxy_core::audit::ClientContext) -> Order {
410 self.created_ip = client.ip.clone();
411 self.created_ptr = client.ptr.clone();
412 self
413 }
414
415 pub async fn insert<'e>(
422 &self,
423 executor: impl Into<crate::sql::Exec<'e>>,
424 ) -> Result<(), sqlx::Error> {
425 let identifiers_json = serde_json::to_string(&self.identifiers)
427 .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
428
429 debug!(event = "db_order_create_started", outcome = "progress", order_id = ?self.id, profile = %self.profile, account_id = ?self.account_id);
430 crate::sql::query(
431 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, not_before, not_after, error, certificate, replaces, created_at, created_ip, created_ptr) \
432 VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?);",
433 )
434 .bind(self.id)
435 .bind(&self.profile)
436 .bind(self.account_id)
437 .bind(self.status.as_str())
438 .bind(identifiers_json)
439 .bind(self.expires)
440 .bind(self.not_before)
441 .bind(self.not_after)
442 .bind(&self.replaces)
443 .bind(self.created_at)
444 .bind(&self.created_ip)
445 .bind(&self.created_ptr)
446 .execute(executor)
447 .await?;
448
449 debug!(event = "db_order_created", outcome = "success", order_id = ?self.id, account_id = ?self.account_id);
450 Ok(())
451 }
452
453 pub async fn create(
456 profile: &str,
457 account_id: Uuid,
458 identifiers: Vec<Identifier>,
459 expires: i64,
460 not_before: Option<i64>,
461 not_after: Option<i64>,
462 database: &Database,
463 ) -> Result<Order, sqlx::Error> {
464 let order = Order::new(
465 profile,
466 account_id,
467 identifiers,
468 expires,
469 not_before,
470 not_after,
471 );
472 order.insert(database).await?;
473 Ok(order)
474 }
475
476 pub async fn find_by_id(id: &str, database: &Database) -> Result<Option<Order>, sqlx::Error> {
479 debug!(event = "db_order_find_by_id_started", outcome = "progress", order_id = ?id);
480 let Some(id) = crate::id::parse(id) else {
481 return Ok(None);
482 };
483 let row = crate::sql::query(concat!("SELECT ", columns!(), " FROM orders WHERE id = ?;"))
484 .bind(id)
485 .fetch_optional(database)
486 .await?;
487
488 let result = row.map(Order::from_row).transpose()?;
489 if result.is_some() {
490 info!(event = "db_order_found_by_id", outcome = "success", order_id = ?id);
491 } else {
492 debug!(event = "db_order_not_found_by_id", outcome = "failure", order_id = ?id);
493 }
494 Ok(result)
495 }
496
497 pub async fn find_by_account(
504 account_id: Uuid,
505 database: &Database,
506 ) -> Result<Vec<Order>, sqlx::Error> {
507 debug!(event = "db_order_find_by_account_started", outcome = "progress", account_id = ?account_id);
508 let rows = crate::sql::query(concat!(
509 "SELECT ",
510 columns!(),
511 " FROM orders WHERE account_id = ? ORDER BY created_at DESC;"
512 ))
513 .bind(account_id)
514 .fetch_all(database)
515 .await?;
516
517 rows.into_iter().map(Order::from_row).collect()
518 }
519
520 pub async fn find_active_by_account(
533 account_id: Uuid,
534 database: &Database,
535 ) -> Result<Vec<Order>, sqlx::Error> {
536 debug!(event = "db_order_find_active_by_account_started", outcome = "progress", account_id = ?account_id);
537 let rows =
538 crate::sql::query(concat!("SELECT ", columns!(), " FROM orders WHERE account_id = ? AND status != 'invalid' AND (status = 'valid' OR expires > ?) ORDER BY created_at DESC;"))
539 .bind(account_id)
540 .bind(now_secs())
541 .fetch_all(database)
542 .await?;
543
544 rows.into_iter().map(Order::from_row).collect()
545 }
546
547 pub async fn search(
567 query: &OrderQuery,
568 database: &Database,
569 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
570 debug!(event = "db_order_search_started",
571 outcome = "progress",
572 profile = ?query.profile,
573 account_id = ?query.account_id,
574 status = ?query.status,
575 identifier = ?query.identifier,
576 identifier_contains = ?query.identifier_contains,
577 cert_serial = ?query.cert_serial,
578 limit = query.limit,
579 offset = query.offset);
580
581 let mut page = crate::sql::Builder::new(
582 database.dialect(),
583 concat!("SELECT ", columns!(), " FROM orders"),
584 );
585 query.push_predicates(&mut page);
586 page.push(" ORDER BY created_at DESC, id DESC LIMIT ");
590 page.push_bind(query.limit);
591 page.push(" OFFSET ");
592 page.push_bind(query.offset);
593
594 let rows = page.build().fetch_all(database).await?;
595 let orders: Vec<Order> = rows
596 .into_iter()
597 .map(Order::from_row)
598 .collect::<Result<_, _>>()?;
599
600 let mut count = crate::sql::Builder::new(database.dialect(), "SELECT COUNT(*) FROM orders");
601 query.push_predicates(&mut count);
602 let total: i64 = count.build().fetch_one(database).await?.try_get::<i64>(0)?;
603
604 Ok((orders, total))
605 }
606
607 pub async fn cleanup(
624 profile: &str,
625 cutoff: i64,
626 database: &Database,
627 ) -> Result<u64, sqlx::Error> {
628 debug!(event = "db_order_cleanup_started", outcome = "progress", profile = %profile, cutoff = cutoff);
629 let removed = crate::sql::query(
630 "DELETE FROM orders WHERE profile = ? AND status != 'valid' AND expires < ?;",
631 )
632 .bind(profile)
633 .bind(cutoff)
634 .execute(database)
635 .await?
636 .rows_affected();
637
638 debug!(event = "db_order_cleanup_completed", outcome = "success", profile = %profile, rows_removed = removed);
639 Ok(removed)
640 }
641
642 pub async fn count_by_account(
648 account_id: Uuid,
649 database: &Database,
650 ) -> Result<i64, sqlx::Error> {
651 let row = crate::sql::query("SELECT COUNT(*) FROM orders WHERE account_id = ?;")
652 .bind(account_id)
653 .fetch_one(database)
654 .await?;
655 row.try_get::<i64>(0)
656 }
657
658 pub async fn delete(id: &str, database: &Database) -> Result<GuardedDelete, sqlx::Error> {
666 debug!(event = "db_order_delete_started", outcome = "progress", order_id = ?id);
667 let Some(id) = crate::id::parse(id) else {
668 return Ok(GuardedDelete::NotFound);
669 };
670 let now = now_secs();
671 let result = crate::sql::query(concat!(
672 "DELETE FROM orders WHERE id = ? AND NOT ",
673 live_certificate!(),
674 ";"
675 ))
676 .bind(id)
677 .bind(now)
678 .execute(database)
679 .await?;
680
681 if result.rows_affected() > 0 {
682 info!(event = "db_order_deleted", outcome = "success", order_id = ?id);
683 return Ok(GuardedDelete::Deleted);
684 }
685
686 let live = Self::count_live_certificates(id, database).await?;
687 if live > 0 {
688 info!(event = "db_order_delete_blocked", outcome = "failure", order_id = ?id, live_certificates = live);
689 Ok(GuardedDelete::LiveCertificates(live))
690 } else {
691 debug!(event = "db_order_delete_missing", outcome = "success", order_id = ?id);
692 Ok(GuardedDelete::NotFound)
693 }
694 }
695
696 pub async fn count_live_certificates(
699 order_id: Uuid,
700 database: &Database,
701 ) -> Result<u64, sqlx::Error> {
702 let live: i64 = crate::sql::query(concat!(
703 "SELECT COUNT(*) FROM orders WHERE id = ? AND ",
704 live_certificate!(),
705 ";"
706 ))
707 .bind(order_id)
708 .bind(now_secs())
709 .fetch_one(database)
710 .await?
711 .try_get(0)?;
712 Ok(live as u64)
713 }
714
715 pub async fn finalize(
733 &mut self,
734 chain: String,
735 cert_serial: String,
736 cert_pubkey: Vec<u8>,
737 cert_not_after: Option<i64>,
738 database: &Database,
739 ) -> Result<(), sqlx::Error> {
740 debug!(event = "db_order_finalize_started", outcome = "progress", order_id = ?self.id);
741 crate::sql::query(
742 "UPDATE orders SET certificate = ?, cert_serial = ?, cert_pubkey = ?, \
743 cert_not_after = ?, status = 'valid' WHERE id = ?;",
744 )
745 .bind(&chain)
746 .bind(&cert_serial)
747 .bind(&cert_pubkey)
748 .bind(cert_not_after)
749 .bind(self.id)
750 .execute(database)
751 .await?;
752
753 self.certificate = Some(chain);
754 self.cert_serial = Some(cert_serial);
755 self.cert_pubkey = Some(cert_pubkey);
756 self.cert_not_after = cert_not_after;
757 self.status = OrderStatus::Valid;
758 debug!(event = "db_order_finalized", outcome = "success", order_id = ?self.id);
759 Ok(())
760 }
761
762 pub async fn find_expiring(
801 profile: Option<&str>,
802 before: i64,
803 limit: i64,
804 offset: i64,
805 database: &Database,
806 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
807 debug!(
808 event = "db_order_find_expiring_started",
809 outcome = "progress",
810 profile = ?profile,
811 before,
812 limit,
813 offset
814 );
815 let mut page = crate::sql::Builder::new(database.dialect(), concat!("SELECT ", columns!()));
816 push_expiring_predicates(profile, before, &mut page);
817 page.push(" ORDER BY cert_not_after ASC, id ASC LIMIT ");
818 page.push_bind(limit);
819 page.push(" OFFSET ");
820 page.push_bind(offset);
821
822 let rows = page.build().fetch_all(database).await?;
823 let orders: Vec<Order> = rows
824 .into_iter()
825 .map(Order::from_row)
826 .collect::<Result<_, _>>()?;
827
828 let mut count = crate::sql::Builder::new(database.dialect(), "SELECT COUNT(*)");
829 push_expiring_predicates(profile, before, &mut count);
830 let total: i64 = count.build().fetch_one(database).await?.try_get::<i64>(0)?;
831
832 Ok((orders, total))
833 }
834
835 pub async fn find_unstamped(
844 profile: &str,
845 limit: i64,
846 database: &Database,
847 ) -> Result<Vec<(Uuid, String)>, sqlx::Error> {
848 let rows = crate::sql::query(
849 "SELECT id, certificate FROM orders WHERE profile = ? \
850 AND certificate IS NOT NULL AND cert_not_after IS NULL LIMIT ?;",
851 )
852 .bind(profile)
853 .bind(limit)
854 .fetch_all(database)
855 .await?;
856
857 rows.into_iter()
858 .map(|row| Ok((row.try_get("id")?, row.try_get("certificate")?)))
859 .collect()
860 }
861
862 pub async fn set_cert_not_after(
870 id: Uuid,
871 cert_not_after: i64,
872 database: &Database,
873 ) -> Result<(), sqlx::Error> {
874 crate::sql::query("UPDATE orders SET cert_not_after = ? WHERE id = ?;")
875 .bind(cert_not_after)
876 .bind(id)
877 .execute(database)
878 .await?;
879 Ok(())
880 }
881
882 pub async fn find_by_cert_serial(
894 profile: &str,
895 serial: &str,
896 database: &Database,
897 ) -> Result<Option<Order>, sqlx::Error> {
898 debug!(event = "db_order_find_by_cert_serial_started", outcome = "progress", profile = %profile, cert_serial = ?serial);
899 let row = crate::sql::query(concat!(
900 "SELECT ",
901 columns!(),
902 " FROM orders WHERE profile = ? AND cert_serial = ?;"
903 ))
904 .bind(profile)
905 .bind(serial)
906 .fetch_optional(database)
907 .await?;
908
909 let result = row.map(Order::from_row).transpose()?;
910 if result.is_some() {
911 info!(event = "db_order_found_by_cert_serial", outcome = "success", cert_serial = ?serial);
912 } else {
913 debug!(event = "db_order_not_found_by_cert_serial", outcome = "failure", cert_serial = ?serial);
914 }
915 Ok(result)
916 }
917
918 pub async fn find_by_replaces(
927 profile: &str,
928 cert_id: &str,
929 database: &Database,
930 ) -> Result<Option<Order>, sqlx::Error> {
931 debug!(event = "db_order_find_by_replaces_started", outcome = "progress", profile = %profile, replaces = %cert_id);
932 let row = crate::sql::query(concat!(
933 "SELECT ",
934 columns!(),
935 " FROM orders WHERE profile = ? AND replaces = ? AND status != 'invalid' LIMIT 1;"
936 ))
937 .bind(profile)
938 .bind(cert_id)
939 .fetch_optional(database)
940 .await?;
941
942 row.map(Order::from_row).transpose()
943 }
944
945 pub async fn revoke(
951 &mut self,
952 reason: Option<i64>,
953 database: &Database,
954 ) -> Result<bool, sqlx::Error> {
955 let now = now_secs();
956 debug!(event = "db_order_revoke_started", outcome = "progress", order_id = ?self.id, reason = ?reason);
957 if !Self::set_revoked(self.id, reason, now, database).await? {
958 return Ok(false);
959 }
960
961 self.revoked_at = Some(now);
962 self.revocation_reason = reason;
963 Ok(true)
964 }
965
966 pub async fn set_revoked<'e>(
980 id: Uuid,
981 reason: Option<i64>,
982 revoked_at: i64,
983 executor: impl Into<crate::sql::Exec<'e>>,
984 ) -> Result<bool, sqlx::Error> {
985 debug!(event = "db_order_revoke_started", outcome = "progress", order_id = ?id, reason = ?reason);
986 let written = crate::sql::query(
987 "UPDATE orders SET revoked_at = ?, revocation_reason = ? \
988 WHERE id = ? AND revoked_at IS NULL;",
989 )
990 .bind(revoked_at)
991 .bind(reason)
992 .bind(id)
993 .execute(executor)
994 .await?
995 .rows_affected();
996 if written == 1 {
997 info!(event = "db_order_revoked", outcome = "success", order_id = ?id, reason = ?reason);
998 }
999 Ok(written == 1)
1000 }
1001
1002 pub async fn set_invalid<'e>(
1016 id: Uuid,
1017 error: &Value,
1018 executor: impl Into<crate::sql::Exec<'e>>,
1019 ) -> Result<bool, sqlx::Error> {
1020 let written = crate::sql::query(
1022 "UPDATE orders SET error = ?, status = 'invalid' \
1023 WHERE id = ? AND status IN ('pending', 'ready', 'processing');",
1024 )
1025 .bind(error.to_string())
1026 .bind(id)
1027 .execute(executor)
1028 .await?
1029 .rows_affected();
1030 Ok(written == 1)
1031 }
1032
1033 pub async fn set_ready<'e>(
1036 id: Uuid,
1037 executor: impl Into<crate::sql::Exec<'e>>,
1038 ) -> Result<bool, sqlx::Error> {
1039 let written = crate::sql::query(
1040 "UPDATE orders SET status = 'ready' WHERE id = ? AND status = 'pending';",
1041 )
1042 .bind(id)
1043 .execute(executor)
1044 .await?
1045 .rows_affected();
1046 Ok(written == 1)
1047 }
1048
1049 pub async fn set_pending<'e>(
1061 id: Uuid,
1062 executor: impl Into<crate::sql::Exec<'e>>,
1063 ) -> Result<bool, sqlx::Error> {
1064 let written = crate::sql::query(
1065 "UPDATE orders SET status = 'pending' WHERE id = ? AND status = 'ready';",
1066 )
1067 .bind(id)
1068 .execute(executor)
1069 .await?
1070 .rows_affected();
1071 Ok(written == 1)
1072 }
1073
1074 pub async fn mark_invalid(
1080 &mut self,
1081 error: Value,
1082 database: &Database,
1083 ) -> Result<bool, sqlx::Error> {
1084 debug!(event = "db_order_mark_invalid_started", outcome = "progress", order_id = ?self.id);
1085 if !Self::set_invalid(self.id, &error, database).await? {
1086 return Ok(false);
1087 }
1088
1089 self.error = Some(error);
1090 self.status = OrderStatus::Invalid;
1091 info!(event = "db_order_marked_invalid", outcome = "failure", order_id = ?self.id);
1092 Ok(true)
1093 }
1094
1095 pub async fn mark_ready(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
1099 debug!(event = "db_order_mark_ready_started", outcome = "progress", order_id = ?self.id);
1100 if !Self::set_ready(self.id, database).await? {
1101 return Ok(false);
1102 }
1103
1104 self.status = OrderStatus::Ready;
1105 info!(event = "db_order_marked_ready", outcome = "success", order_id = ?self.id);
1106 Ok(true)
1107 }
1108
1109 pub async fn claim_for_finalize(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
1135 self.claim_for_finalize_on(database).await
1136 }
1137
1138 pub async fn claim_for_finalize_on<'e>(
1144 &mut self,
1145 executor: impl Into<crate::sql::Exec<'e>>,
1146 ) -> Result<bool, sqlx::Error> {
1147 debug!(event = "db_order_mark_processing_started", outcome = "progress", order_id = ?self.id);
1148 let claimed = crate::sql::query(
1149 "UPDATE orders SET status = 'processing' WHERE id = ? AND status = 'ready';",
1150 )
1151 .bind(self.id)
1152 .execute(executor)
1153 .await?
1154 .rows_affected()
1155 == 1;
1156
1157 if !claimed {
1158 debug!(event = "db_order_finalize_claim_refused", outcome = "failure", order_id = ?self.id);
1159 return Ok(false);
1160 }
1161
1162 self.status = OrderStatus::Processing;
1163 info!(event = "db_order_marked_processing", outcome = "success", order_id = ?self.id);
1164 Ok(true)
1165 }
1166
1167 #[must_use]
1172 pub fn to_json(&self, base_url: &str, authz_ids: &[Uuid]) -> Value {
1173 let mut object = serde_json::Map::new();
1174 object.insert(
1175 "status".to_string(),
1176 Value::String(self.status.as_str().to_string()),
1177 );
1178 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
1179 object.insert(
1180 "identifiers".to_string(),
1181 serde_json::to_value(&self.identifiers).expect("Identifier is always serializable"),
1182 );
1183 if let Some(nb) = self.not_before {
1184 object.insert("notBefore".to_string(), Value::String(rfc3339(nb)));
1185 }
1186 if let Some(na) = self.not_after {
1187 object.insert("notAfter".to_string(), Value::String(rfc3339(na)));
1188 }
1189 let authorizations: Vec<Value> = authz_ids
1190 .iter()
1191 .map(|id| Value::String(format!("{base_url}/authz/{id}")))
1192 .collect();
1193 object.insert("authorizations".to_string(), Value::Array(authorizations));
1194 object.insert(
1195 "finalize".to_string(),
1196 Value::String(format!("{base_url}/order/{}/finalize", self.id)),
1197 );
1198 if self.status == OrderStatus::Valid {
1199 object.insert(
1200 "certificate".to_string(),
1201 Value::String(format!("{base_url}/certificate/{}", self.id)),
1202 );
1203 }
1204 if let Some(ref error) = self.error {
1205 object.insert("error".to_string(), error.clone());
1206 }
1207 if let Some(ref replaces) = self.replaces {
1211 object.insert("replaces".to_string(), Value::String(replaces.clone()));
1212 }
1213 Value::Object(object)
1214 }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::*;
1220 use acme_proxy_core::audit::ClientContext;
1221
1222 #[tokio::test]
1226 async fn with_client_persists_and_an_order_without_one_stays_null() {
1227 let db = std::sync::Arc::new(Database::connect_for_test().await.unwrap());
1228 let account = account_id(&db).await;
1229
1230 let stamped = Order::new(
1231 "default",
1232 account,
1233 vec![Identifier::dns("a.example.com")],
1234 0,
1235 None,
1236 None,
1237 )
1238 .with_client(&ClientContext {
1239 ip: Some("203.0.113.7".to_string()),
1240 ptr: Some("host.example.com".to_string()),
1241 user_agent: Some("lego".to_string()),
1242 request_id: Some("req-1".to_string()),
1243 });
1244 stamped.insert(&db).await.unwrap();
1245 let reloaded = Order::find_by_id(stamped.id.to_string().as_str(), &db)
1246 .await
1247 .unwrap()
1248 .unwrap();
1249 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
1250 assert_eq!(reloaded.created_ptr.as_deref(), Some("host.example.com"));
1251
1252 let bare = Order::new(
1253 "default",
1254 account,
1255 vec![Identifier::dns("b.example.com")],
1256 0,
1257 None,
1258 None,
1259 );
1260 bare.insert(&db).await.unwrap();
1261 let reloaded = Order::find_by_id(bare.id.to_string().as_str(), &db)
1262 .await
1263 .unwrap()
1264 .unwrap();
1265 assert_eq!(reloaded.created_ip, None);
1266 assert_eq!(reloaded.created_ptr, None);
1267
1268 let json = reloaded.to_json("http://localhost:3000", &[]);
1271 let object = json.as_object().unwrap();
1272 assert!(!object.contains_key("createdIp"));
1273 assert!(!object.contains_key("createdPtr"));
1274 assert!(
1275 !stamped
1276 .to_json("http://localhost:3000", &[])
1277 .to_string()
1278 .contains("203.0.113.7")
1279 );
1280 }
1281
1282 use crate::testutil::account_id;
1283 use serde_json::json;
1284 use std::sync::Arc;
1285
1286 #[tokio::test]
1287 async fn create_then_find_by_id_round_trip() {
1288 let db = Arc::new(Database::connect_for_test().await.unwrap());
1289 let acct = account_id(&db).await;
1290
1291 let created = Order::create(
1292 "default",
1293 acct,
1294 vec![Identifier::dns("example.com")],
1295 now_secs() + 3600,
1296 None,
1297 None,
1298 &db,
1299 )
1300 .await
1301 .unwrap();
1302 assert_eq!(created.status, OrderStatus::Pending);
1303
1304 let found = Order::find_by_id(created.id.to_string().as_str(), &db)
1305 .await
1306 .unwrap()
1307 .unwrap();
1308 assert_eq!(found.account_id, acct);
1309 assert_eq!(found.identifiers, vec![Identifier::dns("example.com")]);
1310 assert!(found.certificate.is_none());
1311 }
1312
1313 #[tokio::test]
1314 async fn find_by_account_lists_all() {
1315 let db = Arc::new(Database::connect_for_test().await.unwrap());
1316 let acct = account_id(&db).await;
1317
1318 Order::create(
1319 "default",
1320 acct,
1321 vec![Identifier::dns("a.example")],
1322 now_secs() + 3600,
1323 None,
1324 None,
1325 &db,
1326 )
1327 .await
1328 .unwrap();
1329 Order::create(
1330 "default",
1331 acct,
1332 vec![Identifier::dns("b.example")],
1333 now_secs() + 3600,
1334 None,
1335 None,
1336 &db,
1337 )
1338 .await
1339 .unwrap();
1340
1341 let orders = Order::find_by_account(acct, &db).await.unwrap();
1342 assert_eq!(orders.len(), 2);
1343 }
1344
1345 #[tokio::test]
1346 async fn absent_lookup_returns_none() {
1347 let db = Arc::new(Database::connect_for_test().await.unwrap());
1348 assert!(Order::find_by_id("nope", &db).await.unwrap().is_none());
1349 }
1350
1351 #[tokio::test]
1352 async fn to_json_shape_when_pending() {
1353 let db = Arc::new(Database::connect_for_test().await.unwrap());
1354 let acct = account_id(&db).await;
1355
1356 let order = Order::create(
1357 "default",
1358 acct,
1359 vec![Identifier::dns("example.com")],
1360 now_secs() + 3600,
1361 None,
1362 None,
1363 &db,
1364 )
1365 .await
1366 .unwrap();
1367
1368 let authz = crate::id::mint();
1369 let json = order.to_json("http://localhost:3000", &[authz]);
1370 assert_eq!(json["status"], "pending");
1371 assert_eq!(
1372 json["authorizations"],
1373 json!([format!("http://localhost:3000/authz/{authz}")])
1374 );
1375 assert_eq!(
1376 json["finalize"],
1377 format!("http://localhost:3000/order/{}/finalize", order.id)
1378 );
1379 assert_eq!(
1380 json["identifiers"],
1381 json!([{"type": "dns", "value": "example.com"}])
1382 );
1383 assert!(json.get("certificate").is_none());
1385 assert!(json.get("notBefore").is_none());
1386 assert!(json.get("notAfter").is_none());
1387 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
1389 }
1390
1391 #[tokio::test]
1392 async fn to_json_includes_optional_fields() {
1393 let db = Arc::new(Database::connect_for_test().await.unwrap());
1394 let acct = account_id(&db).await;
1395
1396 let order = Order::create(
1397 "default",
1398 acct,
1399 vec![Identifier::dns("example.com")],
1400 now_secs() + 3600,
1401 Some(now_secs()),
1402 Some(now_secs() + 7200),
1403 &db,
1404 )
1405 .await
1406 .unwrap();
1407
1408 let json = order.to_json("http://localhost:3000", &[]);
1409 assert!(json["notBefore"].as_str().unwrap().ends_with('Z'));
1410 assert!(json["notAfter"].as_str().unwrap().ends_with('Z'));
1411 }
1412
1413 #[tokio::test]
1414 async fn finalize_persists_and_syncs() {
1415 let db = Arc::new(Database::connect_for_test().await.unwrap());
1416 let acct = account_id(&db).await;
1417
1418 let mut order = Order::create(
1419 "default",
1420 acct,
1421 vec![Identifier::dns("example.com")],
1422 now_secs() + 3600,
1423 None,
1424 None,
1425 &db,
1426 )
1427 .await
1428 .unwrap();
1429
1430 order
1431 .finalize(
1432 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1433 "aabbcc".to_string(),
1434 vec![1, 2, 3],
1435 Some(now_secs() + 90 * 24 * 60 * 60),
1436 &db,
1437 )
1438 .await
1439 .unwrap();
1440
1441 assert_eq!(order.status, OrderStatus::Valid);
1443 assert!(order.certificate.is_some());
1444 assert_eq!(order.cert_serial.as_deref(), Some("aabbcc"));
1445 assert_eq!(order.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1446 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1448 .await
1449 .unwrap()
1450 .unwrap();
1451 assert_eq!(reloaded.status, OrderStatus::Valid);
1452 assert_eq!(reloaded.cert_serial.as_deref(), Some("aabbcc"));
1453 assert_eq!(reloaded.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1454 assert!(reloaded.cert_not_after.is_some());
1455 let json = reloaded.to_json("http://localhost:3000", &[]);
1456 assert_eq!(
1457 json["certificate"],
1458 format!("http://localhost:3000/certificate/{}", order.id)
1459 );
1460 }
1461
1462 #[tokio::test]
1465 async fn only_one_caller_can_claim_an_order_for_finalize() {
1466 let db = Arc::new(Database::connect_for_test().await.unwrap());
1467 let acct = account_id(&db).await;
1468
1469 let mut order = Order::create(
1470 "default",
1471 acct,
1472 vec![Identifier::dns("example.com")],
1473 now_secs() + 3600,
1474 None,
1475 None,
1476 &db,
1477 )
1478 .await
1479 .unwrap();
1480 order.mark_ready(&db).await.unwrap();
1481
1482 let mut rival = Order::find_by_id(order.id.to_string().as_str(), &db)
1484 .await
1485 .unwrap()
1486 .unwrap();
1487
1488 assert!(order.claim_for_finalize(&db).await.unwrap());
1489 assert_eq!(order.status, OrderStatus::Processing);
1490
1491 assert!(!rival.claim_for_finalize(&db).await.unwrap());
1494 assert_eq!(rival.status, OrderStatus::Ready);
1495
1496 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1497 .await
1498 .unwrap()
1499 .unwrap();
1500 assert_eq!(reloaded.status, OrderStatus::Processing);
1501 }
1502
1503 #[tokio::test]
1507 async fn an_order_that_is_not_ready_cannot_be_claimed() {
1508 let db = Arc::new(Database::connect_for_test().await.unwrap());
1509 let acct = account_id(&db).await;
1510
1511 for prepare in [
1512 None,
1514 Some(OrderStatus::Valid),
1515 Some(OrderStatus::Invalid),
1516 ] {
1517 let mut order = Order::create(
1518 "default",
1519 acct,
1520 vec![Identifier::dns("example.com")],
1521 now_secs() + 3600,
1522 None,
1523 None,
1524 &db,
1525 )
1526 .await
1527 .unwrap();
1528 match prepare {
1529 None => {}
1530 Some(OrderStatus::Valid) => order
1531 .finalize("chain".to_string(), "aa".to_string(), vec![1], None, &db)
1532 .await
1533 .unwrap(),
1534 Some(_) => assert!(
1535 order
1536 .mark_invalid(serde_json::json!({}), &db)
1537 .await
1538 .unwrap()
1539 ),
1540 }
1541 let before = order.status;
1542
1543 assert!(
1544 !order.claim_for_finalize(&db).await.unwrap(),
1545 "claimed an order in {before}"
1546 );
1547 assert_eq!(order.status, before);
1548 }
1549 }
1550
1551 async fn finalized_order(db: Arc<Database>, serial: &str) -> Order {
1552 let acct = account_id(&db).await;
1553 let mut order = Order::create(
1554 "default",
1555 acct,
1556 vec![Identifier::dns("example.com")],
1557 now_secs() + 3600,
1558 None,
1559 None,
1560 &db,
1561 )
1562 .await
1563 .unwrap();
1564 order
1565 .finalize(
1566 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1567 serial.to_string(),
1568 vec![9, 9, 9],
1569 None,
1570 &db,
1571 )
1572 .await
1573 .unwrap();
1574 order
1575 }
1576
1577 #[tokio::test]
1578 async fn find_by_cert_serial_round_trip() {
1579 let db = Arc::new(Database::connect_for_test().await.unwrap());
1580 let order = finalized_order(db.clone(), "deadbeef").await;
1581
1582 let found = Order::find_by_cert_serial("default", "deadbeef", &db)
1583 .await
1584 .unwrap()
1585 .unwrap();
1586 assert_eq!(found.id, order.id);
1587
1588 assert!(
1589 Order::find_by_cert_serial("default", "unknown", &db)
1590 .await
1591 .unwrap()
1592 .is_none()
1593 );
1594 }
1595
1596 #[tokio::test]
1597 async fn revoke_persists_and_syncs() {
1598 let db = Arc::new(Database::connect_for_test().await.unwrap());
1599 let mut order = finalized_order(db.clone(), "aa11bb22").await;
1600
1601 order.revoke(Some(1), &db).await.unwrap();
1602
1603 assert!(order.revoked_at.is_some());
1605 assert_eq!(order.revocation_reason, Some(1));
1606 assert_eq!(order.status, OrderStatus::Valid);
1607 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1609 .await
1610 .unwrap()
1611 .unwrap();
1612 assert!(reloaded.revoked_at.is_some());
1613 assert_eq!(reloaded.revocation_reason, Some(1));
1614 assert_eq!(reloaded.status, OrderStatus::Valid);
1615 }
1616
1617 #[tokio::test]
1618 async fn revoke_with_no_reason_persists_null() {
1619 let db = Arc::new(Database::connect_for_test().await.unwrap());
1620 let mut order = finalized_order(db.clone(), "cc33dd44").await;
1621
1622 order.revoke(None, &db).await.unwrap();
1623
1624 assert!(order.revoked_at.is_some());
1625 assert!(order.revocation_reason.is_none());
1626 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1627 .await
1628 .unwrap()
1629 .unwrap();
1630 assert!(reloaded.revocation_reason.is_none());
1631 }
1632
1633 #[tokio::test]
1634 async fn to_json_never_exposes_revocation_state() {
1635 let db = Arc::new(Database::connect_for_test().await.unwrap());
1636 let mut order = finalized_order(db.clone(), "ee55ff66").await;
1637 order.revoke(Some(1), &db).await.unwrap();
1638
1639 let json = order.to_json("http://localhost:3000", &[]);
1640 assert!(json.get("revokedAt").is_none());
1641 assert!(json.get("revocationReason").is_none());
1642 assert_eq!(json["status"], "valid");
1643 }
1644
1645 #[tokio::test]
1646 async fn mark_invalid_persists_and_syncs() {
1647 let db = Arc::new(Database::connect_for_test().await.unwrap());
1648 let acct = account_id(&db).await;
1649
1650 let mut order = Order::create(
1651 "default",
1652 acct,
1653 vec![Identifier::dns("example.com")],
1654 now_secs() + 3600,
1655 None,
1656 None,
1657 &db,
1658 )
1659 .await
1660 .unwrap();
1661
1662 let error = json!({
1663 "type": "urn:ietf:params:acme:error:serverInternal",
1664 "detail": "boom",
1665 "status": 500,
1666 });
1667 order.mark_invalid(error.clone(), &db).await.unwrap();
1668
1669 assert_eq!(order.status, OrderStatus::Invalid);
1671 assert_eq!(order.error, Some(error.clone()));
1672 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1674 .await
1675 .unwrap()
1676 .unwrap();
1677 assert_eq!(reloaded.status, OrderStatus::Invalid);
1678 let json = reloaded.to_json("http://localhost:3000", &[]);
1679 assert_eq!(json["error"], error);
1680 }
1681
1682 #[tokio::test]
1683 async fn delete_removes_the_row_and_reports_deleted() {
1684 let db = Arc::new(Database::connect_for_test().await.unwrap());
1685 let acct = account_id(&db).await;
1686 let order = Order::create(
1687 "default",
1688 acct,
1689 vec![Identifier::dns("example.com")],
1690 now_secs() + 3600,
1691 None,
1692 None,
1693 &db,
1694 )
1695 .await
1696 .unwrap();
1697
1698 assert_eq!(
1699 Order::delete(order.id.to_string().as_str(), &db)
1700 .await
1701 .unwrap(),
1702 GuardedDelete::Deleted
1703 );
1704 assert!(
1705 Order::find_by_id(order.id.to_string().as_str(), &db)
1706 .await
1707 .unwrap()
1708 .is_none()
1709 );
1710 }
1711
1712 #[tokio::test]
1713 async fn delete_of_unknown_id_reports_not_found() {
1714 let db = Arc::new(Database::connect_for_test().await.unwrap());
1715 assert_eq!(
1716 Order::delete("nope", &db).await.unwrap(),
1717 GuardedDelete::NotFound
1718 );
1719 }
1720
1721 #[tokio::test]
1722 async fn delete_cascades_to_authorizations_and_challenges() {
1723 let db = Arc::new(Database::connect_for_test().await.unwrap());
1724 let acct = account_id(&db).await;
1725 let order = Order::create(
1726 "default",
1727 acct,
1728 vec![Identifier::dns("example.com")],
1729 now_secs() + 3600,
1730 None,
1731 None,
1732 &db,
1733 )
1734 .await
1735 .unwrap();
1736
1737 let authz = crate::authz::Authorization::create(
1738 order.id,
1739 Identifier::dns("example.com"),
1740 now_secs() + 3600,
1741 &db,
1742 )
1743 .await
1744 .unwrap();
1745 crate::authz::Challenge::create(authz.id, "http-01", &db)
1746 .await
1747 .unwrap();
1748
1749 Order::delete(order.id.to_string().as_str(), &db)
1750 .await
1751 .unwrap();
1752
1753 assert!(
1754 crate::authz::Authorization::find_by_order(order.id, &db)
1755 .await
1756 .unwrap()
1757 .is_empty()
1758 );
1759 assert!(
1760 crate::authz::Challenge::find_by_authz(authz.id, &db)
1761 .await
1762 .unwrap()
1763 .is_empty()
1764 );
1765 }
1766
1767 #[tokio::test]
1772 async fn delete_refuses_an_order_holding_a_live_certificate() {
1773 use crate::testutil::certified_order;
1774
1775 let db = Arc::new(Database::connect_for_test().await.unwrap());
1776 let acct = account_id(&db).await;
1777 let now = now_secs();
1778
1779 for not_after in [Some(now + DAY), None, Some(UNPARSABLE_NOT_AFTER)] {
1780 let order = certified_order(&db, acct, not_after).await;
1781 assert_eq!(
1782 Order::count_live_certificates(order.id, &db).await.unwrap(),
1783 1,
1784 "{not_after:?}"
1785 );
1786 assert_eq!(
1787 Order::delete(order.id.to_string().as_str(), &db)
1788 .await
1789 .unwrap(),
1790 GuardedDelete::LiveCertificates(1),
1791 "{not_after:?}"
1792 );
1793 assert!(
1794 Order::find_by_id(order.id.to_string().as_str(), &db)
1795 .await
1796 .unwrap()
1797 .is_some(),
1798 "a refused delete must leave the row"
1799 );
1800 }
1801
1802 let mut revoked = certified_order(&db, acct, Some(now + DAY)).await;
1803 revoked.revoke(Some(1), &db).await.unwrap();
1804 let expired = certified_order(&db, acct, Some(now - DAY)).await;
1805 for order in [revoked, expired] {
1806 assert_eq!(
1807 Order::count_live_certificates(order.id, &db).await.unwrap(),
1808 0
1809 );
1810 assert_eq!(
1811 Order::delete(order.id.to_string().as_str(), &db)
1812 .await
1813 .unwrap(),
1814 GuardedDelete::Deleted
1815 );
1816 }
1817 }
1818
1819 async fn seed_orders(
1823 db: &Arc<Database>,
1824 profile: &str,
1825 account_id: Uuid,
1826 count: usize,
1827 ) -> Vec<String> {
1828 let base = now_secs();
1829 let mut ids = Vec::new();
1830 for index in 0..count {
1831 let order = Order::create(
1832 profile,
1833 account_id,
1834 vec![Identifier::dns(format!("host-{index}.example.com"))],
1835 base + 3600,
1836 None,
1837 None,
1838 db,
1839 )
1840 .await
1841 .unwrap();
1842 crate::sql::query("UPDATE orders SET created_at = ? WHERE id = ?;")
1843 .bind(base - index as i64)
1844 .bind(order.id)
1845 .execute(db)
1846 .await
1847 .unwrap();
1848 ids.push(order.id);
1849 }
1850 ids.into_iter().map(|v| v.to_string()).collect()
1853 }
1854
1855 fn window(limit: i64, offset: i64) -> OrderQuery {
1856 OrderQuery {
1857 limit,
1858 offset,
1859 ..OrderQuery::default()
1860 }
1861 }
1862
1863 #[tokio::test]
1864 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1865 let db = Arc::new(Database::connect_for_test().await.unwrap());
1866 let acct = account_id(&db).await;
1867 let ids = seed_orders(&db, "default", acct, 5).await;
1868
1869 let (page, total) = Order::search(&window(2, 0), &db).await.unwrap();
1870 assert_eq!(total, 5, "the total must ignore the page window");
1871 assert_eq!(
1872 page.iter().map(|o| o.id.to_string()).collect::<Vec<_>>(),
1873 ids[..2]
1874 );
1875
1876 let (second, total) = Order::search(&window(2, 2), &db).await.unwrap();
1877 assert_eq!(total, 5);
1878 assert_eq!(
1879 second.iter().map(|o| o.id.to_string()).collect::<Vec<_>>(),
1880 ids[2..4]
1881 );
1882
1883 let (last, _) = Order::search(&window(2, 4), &db).await.unwrap();
1885 assert_eq!(last.len(), 1);
1886 let (beyond, total) = Order::search(&window(2, 99), &db).await.unwrap();
1887 assert!(beyond.is_empty());
1888 assert_eq!(total, 5, "a page past the end still reports the real total");
1889 }
1890
1891 #[tokio::test]
1894 async fn paging_one_row_at_a_time_sees_every_order_exactly_once() {
1895 let db = Arc::new(Database::connect_for_test().await.unwrap());
1896 let acct = account_id(&db).await;
1897 let mut expected = Vec::new();
1900 for index in 0..4 {
1901 let order = Order::create(
1902 "default",
1903 acct,
1904 vec![Identifier::dns(format!("same-second-{index}.example.com"))],
1905 now_secs() + 3600,
1906 None,
1907 None,
1908 &db,
1909 )
1910 .await
1911 .unwrap();
1912 expected.push(order.id);
1913 }
1914 expected.sort();
1915
1916 let mut seen = Vec::new();
1917 for offset in 0..4 {
1918 let (page, total) = Order::search(&window(1, offset), &db).await.unwrap();
1919 assert_eq!(total, 4);
1920 assert_eq!(page.len(), 1);
1921 seen.push(page[0].id);
1922 }
1923 seen.sort();
1924 assert_eq!(
1925 seen, expected,
1926 "pages must be disjoint and cover everything"
1927 );
1928 }
1929
1930 #[tokio::test]
1931 async fn search_filters_by_profile_account_and_status_together() {
1932 let db = Arc::new(Database::connect_for_test().await.unwrap());
1933 let acct = account_id(&db).await;
1934 let (other_account, _) = crate::account::Account::find_or_create(
1935 "default",
1936 &[9u8, 9, 9],
1937 vec![],
1938 &ClientContext::default(),
1939 &db,
1940 )
1941 .await
1942 .unwrap();
1943
1944 seed_orders(&db, "default", acct, 3).await;
1945 seed_orders(&db, "default", other_account.id, 2).await;
1946 let mut ready = seed_orders(&db, "default", acct, 1).await;
1947 let ready_id = ready.pop().unwrap();
1948 Order::find_by_id(&ready_id, &db)
1949 .await
1950 .unwrap()
1951 .unwrap()
1952 .mark_ready(&db)
1953 .await
1954 .unwrap();
1955
1956 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1958 assert_eq!(total, 6);
1959
1960 let by_account = OrderQuery {
1962 account_id: Some(acct.clone().to_string()),
1963 ..window(50, 0)
1964 };
1965 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1966 assert_eq!(total, 4);
1967 assert!(rows.iter().all(|o| o.account_id == acct));
1968
1969 let by_status = OrderQuery {
1971 status: Some(OrderStatus::Ready),
1972 ..window(50, 0)
1973 };
1974 let (rows, total) = Order::search(&by_status, &db).await.unwrap();
1975 assert_eq!(total, 1);
1976 assert_eq!(rows[0].id.to_string(), ready_id);
1977
1978 let combined = OrderQuery {
1980 profile: Some("default".to_string()),
1981 account_id: Some(acct.clone().to_string()),
1982 status: Some(OrderStatus::Pending),
1983 ..window(50, 0)
1984 };
1985 let (rows, total) = Order::search(&combined, &db).await.unwrap();
1986 assert_eq!(rows.len(), 3);
1987 assert_eq!(total, 3);
1988
1989 let none = OrderQuery {
1991 profile: Some("no-such-profile".to_string()),
1992 ..window(50, 0)
1993 };
1994 let (rows, total) = Order::search(&none, &db).await.unwrap();
1995 assert!(rows.is_empty());
1996 assert_eq!(total, 0);
1997 }
1998
1999 #[tokio::test]
2000 async fn search_scopes_by_profile() {
2001 let db = Arc::new(Database::connect_for_test().await.unwrap());
2002 let acct = account_id(&db).await;
2003 seed_orders(&db, "default", acct, 2).await;
2004 seed_orders(&db, "other", acct, 3).await;
2005
2006 let scoped = OrderQuery {
2007 profile: Some("other".to_string()),
2008 ..window(50, 0)
2009 };
2010 let (rows, total) = Order::search(&scoped, &db).await.unwrap();
2011 assert_eq!(total, 3);
2012 assert!(rows.iter().all(|o| o.profile == "other"));
2013 }
2014
2015 #[tokio::test]
2024 async fn a_filter_value_is_bound_not_interpolated() {
2025 let db = Arc::new(Database::connect_for_test().await.unwrap());
2026 let acct = account_id(&db).await;
2027 seed_orders(&db, "default", acct, 2).await;
2028
2029 for hostile in ["' OR 1=1 --", "default'; DROP TABLE orders; --"] {
2030 let by_profile = OrderQuery {
2031 profile: Some(hostile.to_string()),
2032 ..window(50, 0)
2033 };
2034 let (rows, total) = Order::search(&by_profile, &db).await.unwrap();
2035 assert!(rows.is_empty(), "the value must be compared, not executed");
2036 assert_eq!(total, 0);
2037
2038 let by_account = OrderQuery {
2039 account_id: Some(hostile.to_string()),
2040 ..window(50, 0)
2041 };
2042 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
2043 assert!(rows.is_empty(), "the value must be compared, not executed");
2044 assert_eq!(total, 0);
2045
2046 for query in [
2047 OrderQuery {
2048 identifier: Some(hostile.to_string()),
2049 ..window(50, 0)
2050 },
2051 OrderQuery {
2052 identifier_contains: Some(hostile.to_string()),
2053 ..window(50, 0)
2054 },
2055 OrderQuery {
2056 cert_serial: Some(hostile.to_string()),
2057 ..window(50, 0)
2058 },
2059 ] {
2060 let (rows, total) = Order::search(&query, &db).await.unwrap();
2061 assert!(rows.is_empty(), "the value must be compared, not executed");
2062 assert_eq!(total, 0);
2063 }
2064 }
2065
2066 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
2068 assert_eq!(total, 2);
2069 }
2070
2071 async fn seed_named(
2074 db: &Arc<Database>,
2075 profile: &str,
2076 account_id: Uuid,
2077 names: &[&str],
2078 ) -> Vec<String> {
2079 let mut ids = Vec::new();
2080 for name in names {
2081 let order = Order::create(
2082 profile,
2083 account_id,
2084 vec![Identifier::dns(*name)],
2085 now_secs() + 3600,
2086 None,
2087 None,
2088 db,
2089 )
2090 .await
2091 .unwrap();
2092 ids.push(order.id.to_string());
2093 }
2094 ids
2095 }
2096
2097 #[tokio::test]
2101 async fn search_filters_by_identifier_exactly() {
2102 let db = Arc::new(Database::connect_for_test().await.unwrap());
2103 let acct = account_id(&db).await;
2104 let ids = seed_named(
2105 &db,
2106 "default",
2107 acct,
2108 &["example.com", "sub.example.com", "evil-example.com"],
2109 )
2110 .await;
2111
2112 for needle in ["example.com", "EXAMPLE.CoM"] {
2113 let query = OrderQuery {
2114 identifier: Some(needle.to_string()),
2115 ..window(50, 0)
2116 };
2117 let (rows, total) = Order::search(&query, &db).await.unwrap();
2118 assert_eq!(total, 1, "needle {needle}");
2119 assert_eq!(rows.len(), 1);
2120 assert_eq!(rows[0].id.to_string(), ids[0]);
2121 }
2122
2123 let query = OrderQuery {
2125 identifier: Some("other.example.com".to_string()),
2126 ..window(50, 0)
2127 };
2128 let (rows, total) = Order::search(&query, &db).await.unwrap();
2129 assert!(rows.is_empty());
2130 assert_eq!(total, 0);
2131 }
2132
2133 #[tokio::test]
2143 async fn an_exact_identifier_hunt_does_not_reach_through_a_wildcard() {
2144 let db = Arc::new(Database::connect_for_test().await.unwrap());
2145 let acct = account_id(&db).await;
2146 seed_named(&db, "default", acct, &["*.example.com"]).await;
2147
2148 let hunt = |needle: &str| {
2149 let query = OrderQuery {
2150 identifier: Some(needle.to_string()),
2151 ..window(50, 0)
2152 };
2153 let db = db.clone();
2154 async move { Order::search(&query, &db).await.unwrap().1 }
2155 };
2156 assert_eq!(hunt("host.example.com").await, 0, "exact means exact");
2157 assert_eq!(hunt("*.example.com").await, 1, "the stored form matches");
2158
2159 let query = OrderQuery {
2161 identifier_contains: Some("example.com".to_string()),
2162 ..window(50, 0)
2163 };
2164 assert_eq!(Order::search(&query, &db).await.unwrap().1, 1);
2165 }
2166
2167 #[tokio::test]
2170 async fn search_filters_by_identifier_substring() {
2171 let db = Arc::new(Database::connect_for_test().await.unwrap());
2172 let acct = account_id(&db).await;
2173 seed_named(
2174 &db,
2175 "default",
2176 acct,
2177 &[
2178 "example.com",
2179 "sub.example.com",
2180 "evil-example.com",
2181 "elsewhere.test",
2182 ],
2183 )
2184 .await;
2185
2186 let query = OrderQuery {
2187 identifier_contains: Some("example.com".to_string()),
2188 ..window(50, 0)
2189 };
2190 let (_, total) = Order::search(&query, &db).await.unwrap();
2191 assert_eq!(total, 3);
2192
2193 let query = OrderQuery {
2194 identifier_contains: Some("%".to_string()),
2195 ..window(50, 0)
2196 };
2197 let (rows, total) = Order::search(&query, &db).await.unwrap();
2198 assert!(rows.is_empty(), "`%` is a literal here, not a wildcard");
2199 assert_eq!(total, 0);
2200 }
2201
2202 #[tokio::test]
2205 async fn search_filters_by_cert_serial() {
2206 let db = Arc::new(Database::connect_for_test().await.unwrap());
2207 let order = finalized_order(db.clone(), "0a1b2c3d").await;
2208
2209 let query = OrderQuery {
2210 cert_serial: Some("0a1b2c3d".to_string()),
2211 ..window(50, 0)
2212 };
2213 let (rows, total) = Order::search(&query, &db).await.unwrap();
2214 assert_eq!(total, 1);
2215 assert_eq!(rows[0].id, order.id);
2216
2217 let query = OrderQuery {
2218 cert_serial: Some("ffffffff".to_string()),
2219 ..window(50, 0)
2220 };
2221 let (rows, total) = Order::search(&query, &db).await.unwrap();
2222 assert!(rows.is_empty());
2223 assert_eq!(total, 0);
2224
2225 let query = OrderQuery {
2232 cert_serial: Some("0A1B2C3D".to_string()),
2233 ..window(50, 0)
2234 };
2235 assert_eq!(
2236 Order::search(&query, &db).await.unwrap().1,
2237 0,
2238 "the model compares raw; the front ends normalize"
2239 );
2240 assert_eq!(
2241 acme_proxy_core::cert::normalize_serial("0A:1B:2C:3D"),
2242 "0a1b2c3d"
2243 );
2244 }
2245
2246 async fn expiring_order(
2250 db: &Database,
2251 account: uuid::Uuid,
2252 names: &[&str],
2253 not_after: Option<i64>,
2254 ) -> Order {
2255 expiring_order_on(db, "default", account, names, not_after).await
2256 }
2257
2258 async fn expiring_order_on(
2260 db: &Database,
2261 profile: &str,
2262 account: uuid::Uuid,
2263 names: &[&str],
2264 not_after: Option<i64>,
2265 ) -> Order {
2266 let mut order = Order::create(
2267 profile,
2268 account,
2269 names.iter().map(|name| Identifier::dns(*name)).collect(),
2270 now_secs() + 3600,
2271 None,
2272 None,
2273 db,
2274 )
2275 .await
2276 .unwrap();
2277 order
2278 .finalize(
2279 "-----BEGIN CERTIFICATE-----\n...".to_string(),
2280 format!("serial-{}", &order.id.to_string()[..8]),
2281 vec![1],
2282 not_after,
2283 db,
2284 )
2285 .await
2286 .unwrap();
2287 order
2288 }
2289
2290 const DAY: i64 = 24 * 60 * 60;
2291
2292 #[tokio::test]
2295 async fn find_expiring_returns_the_window_soonest_first() {
2296 let db = Arc::new(Database::connect_for_test().await.unwrap());
2297 let acct = account_id(&db).await;
2298 let now = now_secs();
2299
2300 let far = expiring_order(&db, acct, &["far.example.com"], Some(now + 60 * DAY)).await;
2301 let soon = expiring_order(&db, acct, &["soon.example.com"], Some(now + 2 * DAY)).await;
2302 let mid = expiring_order(&db, acct, &["mid.example.com"], Some(now + 9 * DAY)).await;
2303
2304 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
2305 .await
2306 .unwrap();
2307
2308 let ids: Vec<String> = page.iter().map(|order| order.id.to_string()).collect();
2309 assert_eq!(ids, vec![soon.id.to_string(), mid.id.to_string()]);
2310 assert_eq!(total, 2);
2311 assert!(
2312 !ids.contains(&far.id.to_string()),
2313 "a certificate outside the window is not expiring yet"
2314 );
2315 }
2316
2317 #[tokio::test]
2319 async fn find_expiring_skips_revoked_unstamped_and_unparsable_rows() {
2320 let db = Arc::new(Database::connect_for_test().await.unwrap());
2321 let acct = account_id(&db).await;
2322 let now = now_secs();
2323
2324 let live = expiring_order(&db, acct, &["live.example.com"], Some(now + DAY)).await;
2325
2326 let mut revoked =
2328 expiring_order(&db, acct, &["revoked.example.com"], Some(now + DAY)).await;
2329 revoked.revoke(Some(1), &db).await.unwrap();
2330
2331 expiring_order(&db, acct, &["old.example.com"], None).await;
2334
2335 let broken = expiring_order(&db, acct, &["broken.example.com"], None).await;
2338 Order::set_cert_not_after(broken.id, -1, &db).await.unwrap();
2339
2340 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
2341 .await
2342 .unwrap();
2343 let ids: Vec<String> = page.iter().map(|order| order.id.to_string()).collect();
2344 assert_eq!(ids, vec![live.id.to_string()]);
2345 assert_eq!(total, 1);
2346 }
2347
2348 #[tokio::test]
2352 async fn find_expiring_reports_the_unpaged_total() {
2353 let db = Arc::new(Database::connect_for_test().await.unwrap());
2354 let acct = account_id(&db).await;
2355 let now = now_secs();
2356 for index in 0..5 {
2357 let name = format!("host-{index}.example.com");
2358 expiring_order(&db, acct, &[name.as_str()], Some(now + DAY)).await;
2359 }
2360
2361 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 2, 0, &db)
2362 .await
2363 .unwrap();
2364 assert_eq!(page.len(), 2);
2365 assert_eq!(total, 5);
2366 }
2367
2368 #[tokio::test]
2371 async fn find_expiring_scopes_by_profile() {
2372 let db = Arc::new(Database::connect_for_test().await.unwrap());
2373 let acct = account_id(&db).await;
2374 let now = now_secs();
2375 expiring_order(&db, acct, &["a.example.com"], Some(now + DAY)).await;
2376
2377 let (page, total) = Order::find_expiring(Some("other"), now + 14 * DAY, 10, 0, &db)
2378 .await
2379 .unwrap();
2380 assert!(page.is_empty());
2381 assert_eq!(total, 0);
2382 }
2383
2384 #[tokio::test]
2388 async fn find_expiring_unscoped_spans_every_profile() {
2389 let db = Arc::new(Database::connect_for_test().await.unwrap());
2390 let acct = account_id(&db).await;
2391 let now = now_secs();
2392
2393 let here =
2394 expiring_order_on(&db, "default", acct, &["a.example.com"], Some(now + DAY)).await;
2395 let there =
2396 expiring_order_on(&db, "other", acct, &["b.example.com"], Some(now + 2 * DAY)).await;
2397
2398 let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
2399 .await
2400 .unwrap();
2401 let ids: Vec<String> = page.iter().map(|order| order.id.to_string()).collect();
2402 assert_eq!(ids, vec![here.id.to_string(), there.id.to_string()]);
2403 assert_eq!(total, 2);
2404
2405 let mut revoked =
2408 expiring_order_on(&db, "other", acct, &["c.example.com"], Some(now + DAY)).await;
2409 revoked.revoke(Some(1), &db).await.unwrap();
2410 let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
2411 .await
2412 .unwrap();
2413 assert_eq!(page.len(), 2);
2414 assert_eq!(total, 2);
2415 }
2416
2417 #[tokio::test]
2421 async fn find_expiring_pages_without_overlap_and_keeps_the_unpaged_total() {
2422 let db = Arc::new(Database::connect_for_test().await.unwrap());
2423 let acct = account_id(&db).await;
2424 let now = now_secs();
2425 for index in 0..5 {
2426 let name = format!("host-{index}.example.com");
2427 expiring_order(&db, acct, &[name.as_str()], Some(now + (index + 1) * DAY)).await;
2430 }
2431
2432 let (first, total) = Order::find_expiring(None, now + 14 * DAY, 2, 0, &db)
2433 .await
2434 .unwrap();
2435 let (second, second_total) = Order::find_expiring(None, now + 14 * DAY, 2, 2, &db)
2436 .await
2437 .unwrap();
2438
2439 assert_eq!(total, 5);
2440 assert_eq!(second_total, 5, "the total is unpaged on every window");
2441 assert_eq!(first.len(), 2);
2442 assert_eq!(second.len(), 2);
2443 let firsts: Vec<String> = first.iter().map(|order| order.id.to_string()).collect();
2444 for order in &second {
2445 assert!(
2446 !firsts.contains(&order.id.to_string()),
2447 "a row must not appear on two pages"
2448 );
2449 }
2450
2451 let (past, _) = Order::find_expiring(None, now + 14 * DAY, 2, 50, &db)
2453 .await
2454 .unwrap();
2455 assert!(past.is_empty());
2456 }
2457
2458 #[tokio::test]
2460 async fn find_unstamped_finds_only_issued_rows_with_no_stamp() {
2461 let db = Arc::new(Database::connect_for_test().await.unwrap());
2462 let acct = account_id(&db).await;
2463
2464 let unstamped = expiring_order(&db, acct, &["old.example.com"], None).await;
2465 expiring_order(&db, acct, &["new.example.com"], Some(now_secs())).await;
2466 Order::create(
2468 "default",
2469 acct,
2470 vec![Identifier::dns("pending.example.com")],
2471 now_secs() + 3600,
2472 None,
2473 None,
2474 &db,
2475 )
2476 .await
2477 .unwrap();
2478
2479 let rows = Order::find_unstamped("default", 10, &db).await.unwrap();
2480 assert_eq!(rows.len(), 1);
2481 assert_eq!(rows[0].0, unstamped.id);
2482
2483 Order::set_cert_not_after(unstamped.id, -1, &db)
2486 .await
2487 .unwrap();
2488 assert!(
2489 Order::find_unstamped("default", 10, &db)
2490 .await
2491 .unwrap()
2492 .is_empty()
2493 );
2494 }
2495}