1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use sqlx::Row;
4use sqlx::sqlite::SqliteRow;
5use time::OffsetDateTime;
6use time::format_description::well_known::Rfc3339;
7use tracing::{debug, info};
8use uuid::Uuid;
9
10use crate::sqlite::db::Database;
11use crate::sqlite::nonce::now_secs;
12use crate::sqlite::status::{self, OrderStatus};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct Identifier {
21 #[serde(rename = "type")]
22 pub typ: String,
23 pub value: String,
24}
25
26impl Identifier {
27 #[must_use]
35 pub fn dns(value: impl Into<String>) -> Self {
36 Self::new("dns", value)
37 }
38
39 #[must_use]
43 pub fn new(typ: impl Into<String>, value: impl Into<String>) -> Self {
44 Self {
45 typ: typ.into(),
46 value: value.into(),
47 }
48 }
49}
50
51#[derive(Debug)]
82pub struct Order {
83 pub id: Uuid,
84 pub profile: String,
89 pub account_id: Uuid,
90 pub status: OrderStatus,
91 pub identifiers: Vec<Identifier>,
92 pub expires: i64,
93 pub not_before: Option<i64>,
94 pub not_after: Option<i64>,
95 pub error: Option<Value>,
96 pub certificate: Option<String>,
97 pub replaces: Option<String>,
103 pub cert_serial: Option<String>,
104 pub cert_pubkey: Option<Vec<u8>>,
105 pub cert_not_after: Option<i64>,
109 pub revoked_at: Option<i64>,
110 pub revocation_reason: Option<i64>,
111 pub created_at: i64,
112 pub created_ip: Option<String>,
119 pub created_ptr: Option<String>,
120}
121
122#[derive(Debug, Clone, Default)]
128pub struct OrderQuery {
129 pub profile: Option<String>,
130 pub account_id: Option<String>,
131 pub status: Option<OrderStatus>,
132 pub limit: i64,
135 pub offset: i64,
136}
137
138impl OrderQuery {
139 fn push_predicates(&self, builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>) {
145 let mut separator = " WHERE ";
146 for (column, value) in [
150 ("profile = ", self.profile.as_deref()),
151 ("status = ", self.status.map(OrderStatus::as_str)),
152 ] {
153 if let Some(value) = value {
154 builder
155 .push(separator)
156 .push(column)
157 .push_bind(value.to_string());
158 separator = " AND ";
159 }
160 }
161
162 if let Some(account_id) = self.account_id.as_deref() {
172 builder
173 .push(separator)
174 .push("account_id = ")
175 .push_bind(super::id::parse(account_id));
176 }
177 }
178}
179
180pub(crate) fn rfc3339(secs: i64) -> String {
185 OffsetDateTime::from_unix_timestamp(secs)
186 .ok()
187 .and_then(|dt| dt.format(&Rfc3339).ok())
188 .unwrap_or_default()
189}
190
191macro_rules! columns {
201 () => {
202 "id, profile, account_id, status, identifiers, expires, not_before, not_after, \
203 error, certificate, replaces, cert_serial, cert_pubkey, cert_not_after, \
204 revoked_at, revocation_reason, created_at, created_ip, created_ptr"
205 };
206}
207
208pub const UNPARSABLE_NOT_AFTER: i64 = -1;
217
218fn push_expiring_predicates(
229 profile: Option<&str>,
230 before: i64,
231 builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>,
232) {
233 builder.push(" FROM orders WHERE certificate IS NOT NULL AND revoked_at IS NULL");
234 builder.push(" AND cert_not_after >= 0 AND cert_not_after <= ");
235 builder.push_bind(before);
236 if let Some(profile) = profile {
237 builder.push(" AND profile = ");
238 builder.push_bind(profile.to_string());
239 }
240}
241
242impl Order {
243 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
244 let identifiers_json: String = row.try_get("identifiers")?;
245 let identifiers: Vec<Identifier> = serde_json::from_str(&identifiers_json)
246 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
247
248 let error_json: Option<String> = row.try_get("error")?;
249 let error: Option<Value> = match error_json {
250 Some(text) => {
251 Some(serde_json::from_str(&text).map_err(|e| sqlx::Error::Decode(Box::new(e)))?)
252 }
253 None => None,
254 };
255
256 Ok(Order {
257 id: row.try_get("id")?,
258 profile: row.try_get("profile")?,
259 account_id: row.try_get("account_id")?,
260 status: status::from_column(row.try_get::<&str, _>("status")?)?,
261 identifiers,
262 expires: row.try_get("expires")?,
263 not_before: row.try_get("not_before")?,
264 not_after: row.try_get("not_after")?,
265 error,
266 certificate: row.try_get("certificate")?,
267 replaces: row.try_get("replaces")?,
268 cert_serial: row.try_get("cert_serial")?,
269 cert_pubkey: row.try_get("cert_pubkey")?,
270 cert_not_after: row.try_get("cert_not_after")?,
271 revoked_at: row.try_get("revoked_at")?,
272 revocation_reason: row.try_get("revocation_reason")?,
273 created_at: row.try_get("created_at")?,
274 created_ip: row.try_get("created_ip")?,
275 created_ptr: row.try_get("created_ptr")?,
276 })
277 }
278
279 pub(crate) fn new(
282 profile: &str,
283 account_id: Uuid,
284 identifiers: Vec<Identifier>,
285 expires: i64,
286 not_before: Option<i64>,
287 not_after: Option<i64>,
288 ) -> Order {
289 Order {
290 id: crate::sqlite::id::mint(),
291 profile: profile.to_string(),
292 account_id,
293 status: OrderStatus::Pending,
294 identifiers,
295 expires,
296 not_before,
297 not_after,
298 error: None,
299 certificate: None,
300 replaces: None,
303 cert_serial: None,
304 cert_pubkey: None,
305 cert_not_after: None,
306 revoked_at: None,
307 revocation_reason: None,
308 created_at: now_secs(),
309 created_ip: None,
315 created_ptr: None,
316 }
317 }
318
319 #[must_use]
326 pub(crate) fn with_client(mut self, client: &crate::audit::ClientContext) -> Order {
327 self.created_ip = client.ip.clone();
328 self.created_ptr = client.ptr.clone();
329 self
330 }
331
332 pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
339 where
340 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
341 {
342 let identifiers_json = serde_json::to_string(&self.identifiers)
344 .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
345
346 debug!(event = "db_order_create_started", outcome = "progress", order_id = ?self.id, profile = %self.profile, account_id = ?self.account_id);
347 sqlx::query(
348 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, not_before, not_after, error, certificate, replaces, created_at, created_ip, created_ptr) \
349 VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?);",
350 )
351 .bind(self.id)
352 .bind(&self.profile)
353 .bind(self.account_id)
354 .bind(self.status.as_str())
355 .bind(identifiers_json)
356 .bind(self.expires)
357 .bind(self.not_before)
358 .bind(self.not_after)
359 .bind(&self.replaces)
360 .bind(self.created_at)
361 .bind(&self.created_ip)
362 .bind(&self.created_ptr)
363 .execute(executor)
364 .await?;
365
366 debug!(event = "db_order_created", outcome = "success", order_id = ?self.id, account_id = ?self.account_id);
367 Ok(())
368 }
369
370 pub async fn create(
373 profile: &str,
374 account_id: Uuid,
375 identifiers: Vec<Identifier>,
376 expires: i64,
377 not_before: Option<i64>,
378 not_after: Option<i64>,
379 database: &Database,
380 ) -> Result<Order, sqlx::Error> {
381 let order = Order::new(
382 profile,
383 account_id,
384 identifiers,
385 expires,
386 not_before,
387 not_after,
388 );
389 order.insert(&database.pool).await?;
390 Ok(order)
391 }
392
393 pub async fn find_by_id(id: &str, database: &Database) -> Result<Option<Order>, sqlx::Error> {
394 debug!(event = "db_order_find_by_id_started", outcome = "progress", order_id = ?id);
395 let Some(id) = crate::sqlite::id::parse(id) else {
396 return Ok(None);
397 };
398 let row = sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE id = ?;"))
399 .bind(id)
400 .fetch_optional(&database.pool)
401 .await?;
402
403 let result = row.map(Order::from_row).transpose()?;
404 if result.is_some() {
405 info!(event = "db_order_found_by_id", outcome = "success", order_id = ?id);
406 } else {
407 debug!(event = "db_order_not_found_by_id", outcome = "failure", order_id = ?id);
408 }
409 Ok(result)
410 }
411
412 pub async fn find_by_account(
419 account_id: Uuid,
420 database: &Database,
421 ) -> Result<Vec<Order>, sqlx::Error> {
422 debug!(event = "db_order_find_by_account_started", outcome = "progress", account_id = ?account_id);
423 let rows = sqlx::query(concat!(
424 "SELECT ",
425 columns!(),
426 " FROM orders WHERE account_id = ? ORDER BY created_at DESC;"
427 ))
428 .bind(account_id)
429 .fetch_all(&database.pool)
430 .await?;
431
432 rows.into_iter().map(Order::from_row).collect()
433 }
434
435 pub async fn find_active_by_account(
448 account_id: Uuid,
449 database: &Database,
450 ) -> Result<Vec<Order>, sqlx::Error> {
451 debug!(event = "db_order_find_active_by_account_started", outcome = "progress", account_id = ?account_id);
452 let rows =
453 sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE account_id = ? AND status != 'invalid' AND (status = 'valid' OR expires > ?) ORDER BY created_at DESC;"))
454 .bind(account_id)
455 .bind(now_secs())
456 .fetch_all(&database.pool)
457 .await?;
458
459 rows.into_iter().map(Order::from_row).collect()
460 }
461
462 pub async fn search(
482 query: &OrderQuery,
483 database: &Database,
484 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
485 debug!(event = "db_order_search_started",
486 outcome = "progress",
487 profile = ?query.profile,
488 account_id = ?query.account_id,
489 status = ?query.status,
490 limit = query.limit,
491 offset = query.offset);
492
493 let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!(), " FROM orders"));
494 query.push_predicates(&mut page);
495 page.push(" ORDER BY created_at DESC, id DESC LIMIT ");
499 page.push_bind(query.limit);
500 page.push(" OFFSET ");
501 page.push_bind(query.offset);
502
503 let rows = page.build().fetch_all(&database.pool).await?;
504 let orders: Vec<Order> = rows
505 .into_iter()
506 .map(Order::from_row)
507 .collect::<Result<_, _>>()?;
508
509 let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*) FROM orders");
510 query.push_predicates(&mut count);
511 let total: i64 = count
512 .build()
513 .fetch_one(&database.pool)
514 .await?
515 .try_get::<i64, _>(0)?;
516
517 Ok((orders, total))
518 }
519
520 pub async fn cleanup(
537 profile: &str,
538 cutoff: i64,
539 database: &Database,
540 ) -> Result<u64, sqlx::Error> {
541 debug!(event = "db_order_cleanup_started", outcome = "progress", profile = %profile, cutoff = cutoff);
542 let removed = sqlx::query(
543 "DELETE FROM orders WHERE profile = ? AND status != 'valid' AND expires < ?;",
544 )
545 .bind(profile)
546 .bind(cutoff)
547 .execute(&database.pool)
548 .await?
549 .rows_affected();
550
551 debug!(event = "db_order_cleanup_completed", outcome = "success", profile = %profile, rows_removed = removed);
552 Ok(removed)
553 }
554
555 pub async fn count_by_account(
561 account_id: Uuid,
562 database: &Database,
563 ) -> Result<i64, sqlx::Error> {
564 let row = sqlx::query("SELECT COUNT(*) FROM orders WHERE account_id = ?;")
565 .bind(account_id)
566 .fetch_one(&database.pool)
567 .await?;
568 row.try_get::<i64, _>(0)
569 }
570
571 pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
574 debug!(event = "db_order_delete_started", outcome = "progress", order_id = ?id);
575 let Some(id) = crate::sqlite::id::parse(id) else {
576 return Ok(false);
577 };
578 let result = sqlx::query("DELETE FROM orders WHERE id = ?;")
579 .bind(id)
580 .execute(&database.pool)
581 .await?;
582
583 let deleted = result.rows_affected() > 0;
584 if deleted {
585 info!(event = "db_order_deleted", outcome = "success", order_id = ?id);
586 } else {
587 debug!(event = "db_order_delete_missing", outcome = "success", order_id = ?id);
588 }
589 Ok(deleted)
590 }
591
592 pub async fn finalize(
610 &mut self,
611 chain: String,
612 cert_serial: String,
613 cert_pubkey: Vec<u8>,
614 cert_not_after: Option<i64>,
615 database: &Database,
616 ) -> Result<(), sqlx::Error> {
617 debug!(event = "db_order_finalize_started", outcome = "progress", order_id = ?self.id);
618 sqlx::query(
619 "UPDATE orders SET certificate = ?, cert_serial = ?, cert_pubkey = ?, \
620 cert_not_after = ?, status = 'valid' WHERE id = ?;",
621 )
622 .bind(&chain)
623 .bind(&cert_serial)
624 .bind(&cert_pubkey)
625 .bind(cert_not_after)
626 .bind(self.id)
627 .execute(&database.pool)
628 .await?;
629
630 self.certificate = Some(chain);
631 self.cert_serial = Some(cert_serial);
632 self.cert_pubkey = Some(cert_pubkey);
633 self.cert_not_after = cert_not_after;
634 self.status = OrderStatus::Valid;
635 debug!(event = "db_order_finalized", outcome = "success", order_id = ?self.id);
636 Ok(())
637 }
638
639 pub async fn find_expiring(
678 profile: Option<&str>,
679 before: i64,
680 limit: i64,
681 offset: i64,
682 database: &Database,
683 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
684 debug!(
685 event = "db_order_find_expiring_started",
686 outcome = "progress",
687 profile = ?profile,
688 before,
689 limit,
690 offset
691 );
692 let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!()));
693 push_expiring_predicates(profile, before, &mut page);
694 page.push(" ORDER BY cert_not_after ASC, id ASC LIMIT ");
695 page.push_bind(limit);
696 page.push(" OFFSET ");
697 page.push_bind(offset);
698
699 let rows = page.build().fetch_all(&database.pool).await?;
700 let orders: Vec<Order> = rows
701 .into_iter()
702 .map(Order::from_row)
703 .collect::<Result<_, _>>()?;
704
705 let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*)");
706 push_expiring_predicates(profile, before, &mut count);
707 let total: i64 = count
708 .build()
709 .fetch_one(&database.pool)
710 .await?
711 .try_get::<i64, _>(0)?;
712
713 Ok((orders, total))
714 }
715
716 pub async fn find_unstamped(
725 profile: &str,
726 limit: i64,
727 database: &Database,
728 ) -> Result<Vec<(Uuid, String)>, sqlx::Error> {
729 let rows = sqlx::query(
730 "SELECT id, certificate FROM orders WHERE profile = ? \
731 AND certificate IS NOT NULL AND cert_not_after IS NULL LIMIT ?;",
732 )
733 .bind(profile)
734 .bind(limit)
735 .fetch_all(&database.pool)
736 .await?;
737
738 rows.into_iter()
739 .map(|row| Ok((row.try_get("id")?, row.try_get("certificate")?)))
740 .collect()
741 }
742
743 pub async fn set_cert_not_after(
751 id: Uuid,
752 cert_not_after: i64,
753 database: &Database,
754 ) -> Result<(), sqlx::Error> {
755 sqlx::query("UPDATE orders SET cert_not_after = ? WHERE id = ?;")
756 .bind(cert_not_after)
757 .bind(id)
758 .execute(&database.pool)
759 .await?;
760 Ok(())
761 }
762
763 pub async fn find_by_cert_serial(
775 profile: &str,
776 serial: &str,
777 database: &Database,
778 ) -> Result<Option<Order>, sqlx::Error> {
779 debug!(event = "db_order_find_by_cert_serial_started", outcome = "progress", profile = %profile, cert_serial = ?serial);
780 let row = sqlx::query(concat!(
781 "SELECT ",
782 columns!(),
783 " FROM orders WHERE profile = ? AND cert_serial = ?;"
784 ))
785 .bind(profile)
786 .bind(serial)
787 .fetch_optional(&database.pool)
788 .await?;
789
790 let result = row.map(Order::from_row).transpose()?;
791 if result.is_some() {
792 info!(event = "db_order_found_by_cert_serial", outcome = "success", cert_serial = ?serial);
793 } else {
794 debug!(event = "db_order_not_found_by_cert_serial", outcome = "failure", cert_serial = ?serial);
795 }
796 Ok(result)
797 }
798
799 pub async fn find_by_replaces(
808 profile: &str,
809 cert_id: &str,
810 database: &Database,
811 ) -> Result<Option<Order>, sqlx::Error> {
812 debug!(event = "db_order_find_by_replaces_started", outcome = "progress", profile = %profile, replaces = %cert_id);
813 let row = sqlx::query(concat!(
814 "SELECT ",
815 columns!(),
816 " FROM orders WHERE profile = ? AND replaces = ? AND status != 'invalid' LIMIT 1;"
817 ))
818 .bind(profile)
819 .bind(cert_id)
820 .fetch_optional(&database.pool)
821 .await?;
822
823 row.map(Order::from_row).transpose()
824 }
825
826 pub async fn revoke(
832 &mut self,
833 reason: Option<i64>,
834 database: &Database,
835 ) -> Result<(), sqlx::Error> {
836 let now = now_secs();
837 debug!(event = "db_order_revoke_started", outcome = "progress", order_id = ?self.id, reason = ?reason);
838 sqlx::query("UPDATE orders SET revoked_at = ?, revocation_reason = ? WHERE id = ?;")
839 .bind(now)
840 .bind(reason)
841 .bind(self.id)
842 .execute(&database.pool)
843 .await?;
844
845 self.revoked_at = Some(now);
846 self.revocation_reason = reason;
847 info!(event = "db_order_revoked", outcome = "success", order_id = ?self.id, reason = ?reason);
848 Ok(())
849 }
850
851 pub(crate) async fn set_invalid<'e, E>(
862 id: Uuid,
863 error: &Value,
864 executor: E,
865 ) -> Result<(), sqlx::Error>
866 where
867 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
868 {
869 sqlx::query("UPDATE orders SET error = ?, status = 'invalid' WHERE id = ?;")
871 .bind(error.to_string())
872 .bind(id)
873 .execute(executor)
874 .await?;
875 Ok(())
876 }
877
878 pub(crate) async fn set_ready<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
880 where
881 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
882 {
883 sqlx::query("UPDATE orders SET status = 'ready' WHERE id = ?;")
884 .bind(id)
885 .execute(executor)
886 .await?;
887 Ok(())
888 }
889
890 pub(crate) async fn set_pending<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
892 where
893 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
894 {
895 sqlx::query("UPDATE orders SET status = 'pending' WHERE id = ?;")
896 .bind(id)
897 .execute(executor)
898 .await?;
899 Ok(())
900 }
901
902 pub async fn mark_invalid(
903 &mut self,
904 error: Value,
905 database: &Database,
906 ) -> Result<(), sqlx::Error> {
907 debug!(event = "db_order_mark_invalid_started", outcome = "progress", order_id = ?self.id);
908 Self::set_invalid(self.id, &error, &database.pool).await?;
909
910 self.error = Some(error);
911 self.status = OrderStatus::Invalid;
912 info!(event = "db_order_marked_invalid", outcome = "failure", order_id = ?self.id);
913 Ok(())
914 }
915
916 pub async fn mark_ready(&mut self, database: &Database) -> Result<(), sqlx::Error> {
920 debug!(event = "db_order_mark_ready_started", outcome = "progress", order_id = ?self.id);
921 Self::set_ready(self.id, &database.pool).await?;
922
923 self.status = OrderStatus::Ready;
924 info!(event = "db_order_marked_ready", outcome = "success", order_id = ?self.id);
925 Ok(())
926 }
927
928 pub async fn mark_pending(&mut self, database: &Database) -> Result<(), sqlx::Error> {
940 debug!(event = "db_order_mark_pending_started", outcome = "progress", order_id = ?self.id);
941 Self::set_pending(self.id, &database.pool).await?;
942
943 self.status = OrderStatus::Pending;
944 info!(event = "db_order_marked_pending", outcome = "success", order_id = ?self.id);
945 Ok(())
946 }
947
948 pub async fn claim_for_finalize(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
973 debug!(event = "db_order_mark_processing_started", outcome = "progress", order_id = ?self.id);
974 let claimed = sqlx::query(
975 "UPDATE orders SET status = 'processing' WHERE id = ? AND status = 'ready';",
976 )
977 .bind(self.id)
978 .execute(&database.pool)
979 .await?
980 .rows_affected()
981 == 1;
982
983 if !claimed {
984 debug!(event = "db_order_finalize_claim_refused", outcome = "failure", order_id = ?self.id);
985 return Ok(false);
986 }
987
988 self.status = OrderStatus::Processing;
989 info!(event = "db_order_marked_processing", outcome = "success", order_id = ?self.id);
990 Ok(true)
991 }
992
993 pub async fn release_finalize_claim(&mut self, database: &Database) -> Result<(), sqlx::Error> {
1006 let released = sqlx::query(
1007 "UPDATE orders SET status = 'ready' WHERE id = ? AND status = 'processing';",
1008 )
1009 .bind(self.id)
1010 .execute(&database.pool)
1011 .await?
1012 .rows_affected()
1013 == 1;
1014
1015 if released {
1016 self.status = OrderStatus::Ready;
1017 }
1018 debug!(event = "db_order_finalize_claim_released", outcome = "success", order_id = ?self.id, released = released);
1019 Ok(())
1020 }
1021
1022 #[must_use]
1027 pub fn to_json(&self, base_url: &str, authz_ids: &[Uuid]) -> Value {
1028 let mut object = serde_json::Map::new();
1029 object.insert(
1030 "status".to_string(),
1031 Value::String(self.status.as_str().to_string()),
1032 );
1033 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
1034 object.insert(
1035 "identifiers".to_string(),
1036 serde_json::to_value(&self.identifiers).expect("Identifier is always serializable"),
1037 );
1038 if let Some(nb) = self.not_before {
1039 object.insert("notBefore".to_string(), Value::String(rfc3339(nb)));
1040 }
1041 if let Some(na) = self.not_after {
1042 object.insert("notAfter".to_string(), Value::String(rfc3339(na)));
1043 }
1044 let authorizations: Vec<Value> = authz_ids
1045 .iter()
1046 .map(|id| Value::String(format!("{base_url}/authz/{id}")))
1047 .collect();
1048 object.insert("authorizations".to_string(), Value::Array(authorizations));
1049 object.insert(
1050 "finalize".to_string(),
1051 Value::String(format!("{base_url}/order/{}/finalize", self.id)),
1052 );
1053 if self.status == OrderStatus::Valid {
1054 object.insert(
1055 "certificate".to_string(),
1056 Value::String(format!("{base_url}/certificate/{}", self.id)),
1057 );
1058 }
1059 if let Some(ref error) = self.error {
1060 object.insert("error".to_string(), error.clone());
1061 }
1062 if let Some(ref replaces) = self.replaces {
1066 object.insert("replaces".to_string(), Value::String(replaces.clone()));
1067 }
1068 Value::Object(object)
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075 use crate::audit::ClientContext;
1076
1077 #[tokio::test]
1081 async fn with_client_persists_and_an_order_without_one_stays_null() {
1082 let db = std::sync::Arc::new(Database::connect_in_memory().await.unwrap());
1083 let account = account_id(&db).await;
1084
1085 let stamped = Order::new(
1086 "default",
1087 account,
1088 vec![Identifier::dns("a.example.com")],
1089 0,
1090 None,
1091 None,
1092 )
1093 .with_client(&ClientContext {
1094 ip: Some("203.0.113.7".to_string()),
1095 ptr: Some("host.example.com".to_string()),
1096 user_agent: Some("lego".to_string()),
1097 request_id: Some("req-1".to_string()),
1098 });
1099 stamped.insert(&db.pool).await.unwrap();
1100 let reloaded = Order::find_by_id(stamped.id.to_string().as_str(), &db)
1101 .await
1102 .unwrap()
1103 .unwrap();
1104 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
1105 assert_eq!(reloaded.created_ptr.as_deref(), Some("host.example.com"));
1106
1107 let bare = Order::new(
1108 "default",
1109 account,
1110 vec![Identifier::dns("b.example.com")],
1111 0,
1112 None,
1113 None,
1114 );
1115 bare.insert(&db.pool).await.unwrap();
1116 let reloaded = Order::find_by_id(bare.id.to_string().as_str(), &db)
1117 .await
1118 .unwrap()
1119 .unwrap();
1120 assert_eq!(reloaded.created_ip, None);
1121 assert_eq!(reloaded.created_ptr, None);
1122
1123 let json = reloaded.to_json("http://localhost:3000", &[]);
1126 let object = json.as_object().unwrap();
1127 assert!(!object.contains_key("createdIp"));
1128 assert!(!object.contains_key("createdPtr"));
1129 assert!(
1130 !stamped
1131 .to_json("http://localhost:3000", &[])
1132 .to_string()
1133 .contains("203.0.113.7")
1134 );
1135 }
1136
1137 use crate::testutil::account_id;
1138 use serde_json::json;
1139 use std::sync::Arc;
1140
1141 #[tokio::test]
1142 async fn create_then_find_by_id_round_trip() {
1143 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1144 let acct = account_id(&db).await;
1145
1146 let created = Order::create(
1147 "default",
1148 acct,
1149 vec![Identifier::dns("example.com")],
1150 now_secs() + 3600,
1151 None,
1152 None,
1153 &db,
1154 )
1155 .await
1156 .unwrap();
1157 assert_eq!(created.status, OrderStatus::Pending);
1158
1159 let found = Order::find_by_id(created.id.to_string().as_str(), &db)
1160 .await
1161 .unwrap()
1162 .unwrap();
1163 assert_eq!(found.account_id, acct);
1164 assert_eq!(found.identifiers, vec![Identifier::dns("example.com")]);
1165 assert!(found.certificate.is_none());
1166 }
1167
1168 #[tokio::test]
1169 async fn find_by_account_lists_all() {
1170 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1171 let acct = account_id(&db).await;
1172
1173 Order::create(
1174 "default",
1175 acct,
1176 vec![Identifier::dns("a.example")],
1177 now_secs() + 3600,
1178 None,
1179 None,
1180 &db,
1181 )
1182 .await
1183 .unwrap();
1184 Order::create(
1185 "default",
1186 acct,
1187 vec![Identifier::dns("b.example")],
1188 now_secs() + 3600,
1189 None,
1190 None,
1191 &db,
1192 )
1193 .await
1194 .unwrap();
1195
1196 let orders = Order::find_by_account(acct, &db).await.unwrap();
1197 assert_eq!(orders.len(), 2);
1198 }
1199
1200 #[tokio::test]
1201 async fn absent_lookup_returns_none() {
1202 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1203 assert!(Order::find_by_id("nope", &db).await.unwrap().is_none());
1204 }
1205
1206 #[tokio::test]
1207 async fn to_json_shape_when_pending() {
1208 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1209 let acct = account_id(&db).await;
1210
1211 let order = Order::create(
1212 "default",
1213 acct,
1214 vec![Identifier::dns("example.com")],
1215 now_secs() + 3600,
1216 None,
1217 None,
1218 &db,
1219 )
1220 .await
1221 .unwrap();
1222
1223 let authz = crate::sqlite::id::mint();
1224 let json = order.to_json("http://localhost:3000", &[authz]);
1225 assert_eq!(json["status"], "pending");
1226 assert_eq!(
1227 json["authorizations"],
1228 json!([format!("http://localhost:3000/authz/{authz}")])
1229 );
1230 assert_eq!(
1231 json["finalize"],
1232 format!("http://localhost:3000/order/{}/finalize", order.id)
1233 );
1234 assert_eq!(
1235 json["identifiers"],
1236 json!([{"type": "dns", "value": "example.com"}])
1237 );
1238 assert!(json.get("certificate").is_none());
1240 assert!(json.get("notBefore").is_none());
1241 assert!(json.get("notAfter").is_none());
1242 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
1244 }
1245
1246 #[tokio::test]
1247 async fn to_json_includes_optional_fields() {
1248 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1249 let acct = account_id(&db).await;
1250
1251 let order = Order::create(
1252 "default",
1253 acct,
1254 vec![Identifier::dns("example.com")],
1255 now_secs() + 3600,
1256 Some(now_secs()),
1257 Some(now_secs() + 7200),
1258 &db,
1259 )
1260 .await
1261 .unwrap();
1262
1263 let json = order.to_json("http://localhost:3000", &[]);
1264 assert!(json["notBefore"].as_str().unwrap().ends_with('Z'));
1265 assert!(json["notAfter"].as_str().unwrap().ends_with('Z'));
1266 }
1267
1268 #[tokio::test]
1269 async fn finalize_persists_and_syncs() {
1270 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1271 let acct = account_id(&db).await;
1272
1273 let mut order = Order::create(
1274 "default",
1275 acct,
1276 vec![Identifier::dns("example.com")],
1277 now_secs() + 3600,
1278 None,
1279 None,
1280 &db,
1281 )
1282 .await
1283 .unwrap();
1284
1285 order
1286 .finalize(
1287 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1288 "aabbcc".to_string(),
1289 vec![1, 2, 3],
1290 Some(now_secs() + 90 * 24 * 60 * 60),
1291 &db,
1292 )
1293 .await
1294 .unwrap();
1295
1296 assert_eq!(order.status, OrderStatus::Valid);
1298 assert!(order.certificate.is_some());
1299 assert_eq!(order.cert_serial.as_deref(), Some("aabbcc"));
1300 assert_eq!(order.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1301 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1303 .await
1304 .unwrap()
1305 .unwrap();
1306 assert_eq!(reloaded.status, OrderStatus::Valid);
1307 assert_eq!(reloaded.cert_serial.as_deref(), Some("aabbcc"));
1308 assert_eq!(reloaded.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1309 assert!(reloaded.cert_not_after.is_some());
1310 let json = reloaded.to_json("http://localhost:3000", &[]);
1311 assert_eq!(
1312 json["certificate"],
1313 format!("http://localhost:3000/certificate/{}", order.id)
1314 );
1315 }
1316
1317 #[tokio::test]
1320 async fn only_one_caller_can_claim_an_order_for_finalize() {
1321 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1322 let acct = account_id(&db).await;
1323
1324 let mut order = Order::create(
1325 "default",
1326 acct,
1327 vec![Identifier::dns("example.com")],
1328 now_secs() + 3600,
1329 None,
1330 None,
1331 &db,
1332 )
1333 .await
1334 .unwrap();
1335 order.mark_ready(&db).await.unwrap();
1336
1337 let mut rival = Order::find_by_id(order.id.to_string().as_str(), &db)
1339 .await
1340 .unwrap()
1341 .unwrap();
1342
1343 assert!(order.claim_for_finalize(&db).await.unwrap());
1344 assert_eq!(order.status, OrderStatus::Processing);
1345
1346 assert!(!rival.claim_for_finalize(&db).await.unwrap());
1349 assert_eq!(rival.status, OrderStatus::Ready);
1350
1351 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1352 .await
1353 .unwrap()
1354 .unwrap();
1355 assert_eq!(reloaded.status, OrderStatus::Processing);
1356 }
1357
1358 #[tokio::test]
1362 async fn an_order_that_is_not_ready_cannot_be_claimed() {
1363 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1364 let acct = account_id(&db).await;
1365
1366 for prepare in [
1367 None,
1369 Some(OrderStatus::Valid),
1370 Some(OrderStatus::Invalid),
1371 ] {
1372 let mut order = Order::create(
1373 "default",
1374 acct,
1375 vec![Identifier::dns("example.com")],
1376 now_secs() + 3600,
1377 None,
1378 None,
1379 &db,
1380 )
1381 .await
1382 .unwrap();
1383 match prepare {
1384 None => {}
1385 Some(OrderStatus::Valid) => order
1386 .finalize("chain".to_string(), "aa".to_string(), vec![1], None, &db)
1387 .await
1388 .unwrap(),
1389 Some(_) => order
1390 .mark_invalid(serde_json::json!({}), &db)
1391 .await
1392 .unwrap(),
1393 }
1394 let before = order.status;
1395
1396 assert!(
1397 !order.claim_for_finalize(&db).await.unwrap(),
1398 "claimed an order in {before}"
1399 );
1400 assert_eq!(order.status, before);
1401 }
1402 }
1403
1404 #[tokio::test]
1407 async fn releasing_a_claim_restores_ready_but_never_overrides_a_demotion() {
1408 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1409 let acct = account_id(&db).await;
1410
1411 let mut order = Order::create(
1412 "default",
1413 acct,
1414 vec![Identifier::dns("example.com")],
1415 now_secs() + 3600,
1416 None,
1417 None,
1418 &db,
1419 )
1420 .await
1421 .unwrap();
1422 order.mark_ready(&db).await.unwrap();
1423 assert!(order.claim_for_finalize(&db).await.unwrap());
1424
1425 order.release_finalize_claim(&db).await.unwrap();
1426 assert_eq!(order.status, OrderStatus::Ready);
1427 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1428 .await
1429 .unwrap()
1430 .unwrap();
1431 assert_eq!(reloaded.status, OrderStatus::Ready);
1432
1433 assert!(order.claim_for_finalize(&db).await.unwrap());
1437 order.mark_pending(&db).await.unwrap();
1438 order.release_finalize_claim(&db).await.unwrap();
1439 assert_eq!(order.status, OrderStatus::Pending);
1440 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1441 .await
1442 .unwrap()
1443 .unwrap();
1444 assert_eq!(reloaded.status, OrderStatus::Pending);
1445 }
1446
1447 async fn finalized_order(db: Arc<Database>, serial: &str) -> Order {
1448 let acct = account_id(&db).await;
1449 let mut order = Order::create(
1450 "default",
1451 acct,
1452 vec![Identifier::dns("example.com")],
1453 now_secs() + 3600,
1454 None,
1455 None,
1456 &db,
1457 )
1458 .await
1459 .unwrap();
1460 order
1461 .finalize(
1462 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1463 serial.to_string(),
1464 vec![9, 9, 9],
1465 None,
1466 &db,
1467 )
1468 .await
1469 .unwrap();
1470 order
1471 }
1472
1473 #[tokio::test]
1474 async fn find_by_cert_serial_round_trip() {
1475 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1476 let order = finalized_order(db.clone(), "deadbeef").await;
1477
1478 let found = Order::find_by_cert_serial("default", "deadbeef", &db)
1479 .await
1480 .unwrap()
1481 .unwrap();
1482 assert_eq!(found.id, order.id);
1483
1484 assert!(
1485 Order::find_by_cert_serial("default", "unknown", &db)
1486 .await
1487 .unwrap()
1488 .is_none()
1489 );
1490 }
1491
1492 #[tokio::test]
1493 async fn revoke_persists_and_syncs() {
1494 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1495 let mut order = finalized_order(db.clone(), "aa11bb22").await;
1496
1497 order.revoke(Some(1), &db).await.unwrap();
1498
1499 assert!(order.revoked_at.is_some());
1501 assert_eq!(order.revocation_reason, Some(1));
1502 assert_eq!(order.status, OrderStatus::Valid);
1503 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1505 .await
1506 .unwrap()
1507 .unwrap();
1508 assert!(reloaded.revoked_at.is_some());
1509 assert_eq!(reloaded.revocation_reason, Some(1));
1510 assert_eq!(reloaded.status, OrderStatus::Valid);
1511 }
1512
1513 #[tokio::test]
1514 async fn revoke_with_no_reason_persists_null() {
1515 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1516 let mut order = finalized_order(db.clone(), "cc33dd44").await;
1517
1518 order.revoke(None, &db).await.unwrap();
1519
1520 assert!(order.revoked_at.is_some());
1521 assert!(order.revocation_reason.is_none());
1522 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1523 .await
1524 .unwrap()
1525 .unwrap();
1526 assert!(reloaded.revocation_reason.is_none());
1527 }
1528
1529 #[tokio::test]
1530 async fn to_json_never_exposes_revocation_state() {
1531 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1532 let mut order = finalized_order(db.clone(), "ee55ff66").await;
1533 order.revoke(Some(1), &db).await.unwrap();
1534
1535 let json = order.to_json("http://localhost:3000", &[]);
1536 assert!(json.get("revokedAt").is_none());
1537 assert!(json.get("revocationReason").is_none());
1538 assert_eq!(json["status"], "valid");
1539 }
1540
1541 #[tokio::test]
1542 async fn mark_invalid_persists_and_syncs() {
1543 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1544 let acct = account_id(&db).await;
1545
1546 let mut order = Order::create(
1547 "default",
1548 acct,
1549 vec![Identifier::dns("example.com")],
1550 now_secs() + 3600,
1551 None,
1552 None,
1553 &db,
1554 )
1555 .await
1556 .unwrap();
1557
1558 let error = json!({
1559 "type": "urn:ietf:params:acme:error:serverInternal",
1560 "detail": "boom",
1561 "status": 500,
1562 });
1563 order.mark_invalid(error.clone(), &db).await.unwrap();
1564
1565 assert_eq!(order.status, OrderStatus::Invalid);
1567 assert_eq!(order.error, Some(error.clone()));
1568 let reloaded = Order::find_by_id(order.id.to_string().as_str(), &db)
1570 .await
1571 .unwrap()
1572 .unwrap();
1573 assert_eq!(reloaded.status, OrderStatus::Invalid);
1574 let json = reloaded.to_json("http://localhost:3000", &[]);
1575 assert_eq!(json["error"], error);
1576 }
1577
1578 #[tokio::test]
1579 async fn delete_removes_the_row_and_reports_true() {
1580 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1581 let acct = account_id(&db).await;
1582 let order = Order::create(
1583 "default",
1584 acct,
1585 vec![Identifier::dns("example.com")],
1586 now_secs() + 3600,
1587 None,
1588 None,
1589 &db,
1590 )
1591 .await
1592 .unwrap();
1593
1594 assert!(
1595 Order::delete(order.id.to_string().as_str(), &db)
1596 .await
1597 .unwrap()
1598 );
1599 assert!(
1600 Order::find_by_id(order.id.to_string().as_str(), &db)
1601 .await
1602 .unwrap()
1603 .is_none()
1604 );
1605 }
1606
1607 #[tokio::test]
1608 async fn delete_of_unknown_id_reports_false() {
1609 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1610 assert!(!Order::delete("nope", &db).await.unwrap());
1611 }
1612
1613 #[tokio::test]
1614 async fn delete_cascades_to_authorizations_and_challenges() {
1615 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1616 let acct = account_id(&db).await;
1617 let order = Order::create(
1618 "default",
1619 acct,
1620 vec![Identifier::dns("example.com")],
1621 now_secs() + 3600,
1622 None,
1623 None,
1624 &db,
1625 )
1626 .await
1627 .unwrap();
1628
1629 let authz = crate::sqlite::authz::Authorization::create(
1630 order.id,
1631 Identifier::dns("example.com"),
1632 now_secs() + 3600,
1633 &db,
1634 )
1635 .await
1636 .unwrap();
1637 crate::sqlite::authz::Challenge::create(authz.id, "http-01", &db)
1638 .await
1639 .unwrap();
1640
1641 Order::delete(order.id.to_string().as_str(), &db)
1642 .await
1643 .unwrap();
1644
1645 assert!(
1646 crate::sqlite::authz::Authorization::find_by_order(order.id, &db)
1647 .await
1648 .unwrap()
1649 .is_empty()
1650 );
1651 assert!(
1652 crate::sqlite::authz::Challenge::find_by_authz(authz.id, &db)
1653 .await
1654 .unwrap()
1655 .is_empty()
1656 );
1657 }
1658
1659 async fn seed_orders(
1663 db: &Arc<Database>,
1664 profile: &str,
1665 account_id: Uuid,
1666 count: usize,
1667 ) -> Vec<String> {
1668 let base = now_secs();
1669 let mut ids = Vec::new();
1670 for index in 0..count {
1671 let order = Order::create(
1672 profile,
1673 account_id,
1674 vec![Identifier::dns(format!("host-{index}.example.com"))],
1675 base + 3600,
1676 None,
1677 None,
1678 db,
1679 )
1680 .await
1681 .unwrap();
1682 sqlx::query("UPDATE orders SET created_at = ? WHERE id = ?;")
1683 .bind(base - index as i64)
1684 .bind(order.id)
1685 .execute(&db.pool)
1686 .await
1687 .unwrap();
1688 ids.push(order.id);
1689 }
1690 ids.into_iter().map(|v| v.to_string()).collect()
1693 }
1694
1695 fn window(limit: i64, offset: i64) -> OrderQuery {
1696 OrderQuery {
1697 limit,
1698 offset,
1699 ..OrderQuery::default()
1700 }
1701 }
1702
1703 #[tokio::test]
1704 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1705 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1706 let acct = account_id(&db).await;
1707 let ids = seed_orders(&db, "default", acct, 5).await;
1708
1709 let (page, total) = Order::search(&window(2, 0), &db).await.unwrap();
1710 assert_eq!(total, 5, "the total must ignore the page window");
1711 assert_eq!(
1712 page.iter().map(|o| o.id.to_string()).collect::<Vec<_>>(),
1713 ids[..2]
1714 );
1715
1716 let (second, total) = Order::search(&window(2, 2), &db).await.unwrap();
1717 assert_eq!(total, 5);
1718 assert_eq!(
1719 second.iter().map(|o| o.id.to_string()).collect::<Vec<_>>(),
1720 ids[2..4]
1721 );
1722
1723 let (last, _) = Order::search(&window(2, 4), &db).await.unwrap();
1725 assert_eq!(last.len(), 1);
1726 let (beyond, total) = Order::search(&window(2, 99), &db).await.unwrap();
1727 assert!(beyond.is_empty());
1728 assert_eq!(total, 5, "a page past the end still reports the real total");
1729 }
1730
1731 #[tokio::test]
1734 async fn paging_one_row_at_a_time_sees_every_order_exactly_once() {
1735 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1736 let acct = account_id(&db).await;
1737 let mut expected = Vec::new();
1740 for index in 0..4 {
1741 let order = Order::create(
1742 "default",
1743 acct,
1744 vec![Identifier::dns(format!("same-second-{index}.example.com"))],
1745 now_secs() + 3600,
1746 None,
1747 None,
1748 &db,
1749 )
1750 .await
1751 .unwrap();
1752 expected.push(order.id);
1753 }
1754 expected.sort();
1755
1756 let mut seen = Vec::new();
1757 for offset in 0..4 {
1758 let (page, total) = Order::search(&window(1, offset), &db).await.unwrap();
1759 assert_eq!(total, 4);
1760 assert_eq!(page.len(), 1);
1761 seen.push(page[0].id);
1762 }
1763 seen.sort();
1764 assert_eq!(
1765 seen, expected,
1766 "pages must be disjoint and cover everything"
1767 );
1768 }
1769
1770 #[tokio::test]
1771 async fn search_filters_by_profile_account_and_status_together() {
1772 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1773 let acct = account_id(&db).await;
1774 let (other_account, _) = crate::sqlite::account::Account::find_or_create(
1775 "default",
1776 &[9u8, 9, 9],
1777 vec![],
1778 &ClientContext::default(),
1779 &db,
1780 )
1781 .await
1782 .unwrap();
1783
1784 seed_orders(&db, "default", acct, 3).await;
1785 seed_orders(&db, "default", other_account.id, 2).await;
1786 let mut ready = seed_orders(&db, "default", acct, 1).await;
1787 let ready_id = ready.pop().unwrap();
1788 Order::find_by_id(&ready_id, &db)
1789 .await
1790 .unwrap()
1791 .unwrap()
1792 .mark_ready(&db)
1793 .await
1794 .unwrap();
1795
1796 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1798 assert_eq!(total, 6);
1799
1800 let by_account = OrderQuery {
1802 account_id: Some(acct.clone().to_string()),
1803 ..window(50, 0)
1804 };
1805 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1806 assert_eq!(total, 4);
1807 assert!(rows.iter().all(|o| o.account_id == acct));
1808
1809 let by_status = OrderQuery {
1811 status: Some(OrderStatus::Ready),
1812 ..window(50, 0)
1813 };
1814 let (rows, total) = Order::search(&by_status, &db).await.unwrap();
1815 assert_eq!(total, 1);
1816 assert_eq!(rows[0].id.to_string(), ready_id);
1817
1818 let combined = OrderQuery {
1820 profile: Some("default".to_string()),
1821 account_id: Some(acct.clone().to_string()),
1822 status: Some(OrderStatus::Pending),
1823 limit: 50,
1824 offset: 0,
1825 };
1826 let (rows, total) = Order::search(&combined, &db).await.unwrap();
1827 assert_eq!(rows.len(), 3);
1828 assert_eq!(total, 3);
1829
1830 let none = OrderQuery {
1832 profile: Some("no-such-profile".to_string()),
1833 ..window(50, 0)
1834 };
1835 let (rows, total) = Order::search(&none, &db).await.unwrap();
1836 assert!(rows.is_empty());
1837 assert_eq!(total, 0);
1838 }
1839
1840 #[tokio::test]
1841 async fn search_scopes_by_profile() {
1842 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1843 let acct = account_id(&db).await;
1844 seed_orders(&db, "default", acct, 2).await;
1845 seed_orders(&db, "other", acct, 3).await;
1846
1847 let scoped = OrderQuery {
1848 profile: Some("other".to_string()),
1849 ..window(50, 0)
1850 };
1851 let (rows, total) = Order::search(&scoped, &db).await.unwrap();
1852 assert_eq!(total, 3);
1853 assert!(rows.iter().all(|o| o.profile == "other"));
1854 }
1855
1856 #[tokio::test]
1864 async fn a_filter_value_is_bound_not_interpolated() {
1865 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1866 let acct = account_id(&db).await;
1867 seed_orders(&db, "default", acct, 2).await;
1868
1869 for hostile in ["' OR 1=1 --", "default'; DROP TABLE orders; --"] {
1870 let by_profile = OrderQuery {
1871 profile: Some(hostile.to_string()),
1872 ..window(50, 0)
1873 };
1874 let (rows, total) = Order::search(&by_profile, &db).await.unwrap();
1875 assert!(rows.is_empty(), "the value must be compared, not executed");
1876 assert_eq!(total, 0);
1877
1878 let by_account = OrderQuery {
1879 account_id: Some(hostile.to_string()),
1880 ..window(50, 0)
1881 };
1882 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1883 assert!(rows.is_empty(), "the value must be compared, not executed");
1884 assert_eq!(total, 0);
1885 }
1886
1887 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1889 assert_eq!(total, 2);
1890 }
1891
1892 async fn expiring_order(
1896 db: &Database,
1897 account: uuid::Uuid,
1898 names: &[&str],
1899 not_after: Option<i64>,
1900 ) -> Order {
1901 expiring_order_on(db, "default", account, names, not_after).await
1902 }
1903
1904 async fn expiring_order_on(
1906 db: &Database,
1907 profile: &str,
1908 account: uuid::Uuid,
1909 names: &[&str],
1910 not_after: Option<i64>,
1911 ) -> Order {
1912 let mut order = Order::create(
1913 profile,
1914 account,
1915 names.iter().map(|name| Identifier::dns(*name)).collect(),
1916 now_secs() + 3600,
1917 None,
1918 None,
1919 db,
1920 )
1921 .await
1922 .unwrap();
1923 order
1924 .finalize(
1925 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1926 format!("serial-{}", &order.id.to_string()[..8]),
1927 vec![1],
1928 not_after,
1929 db,
1930 )
1931 .await
1932 .unwrap();
1933 order
1934 }
1935
1936 const DAY: i64 = 24 * 60 * 60;
1937
1938 #[tokio::test]
1941 async fn find_expiring_returns_the_window_soonest_first() {
1942 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1943 let acct = account_id(&db).await;
1944 let now = now_secs();
1945
1946 let far = expiring_order(&db, acct, &["far.example.com"], Some(now + 60 * DAY)).await;
1947 let soon = expiring_order(&db, acct, &["soon.example.com"], Some(now + 2 * DAY)).await;
1948 let mid = expiring_order(&db, acct, &["mid.example.com"], Some(now + 9 * DAY)).await;
1949
1950 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
1951 .await
1952 .unwrap();
1953
1954 let ids: Vec<String> = page.iter().map(|order| order.id.to_string()).collect();
1955 assert_eq!(ids, vec![soon.id.to_string(), mid.id.to_string()]);
1956 assert_eq!(total, 2);
1957 assert!(
1958 !ids.contains(&far.id.to_string()),
1959 "a certificate outside the window is not expiring yet"
1960 );
1961 }
1962
1963 #[tokio::test]
1965 async fn find_expiring_skips_revoked_unstamped_and_unparsable_rows() {
1966 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1967 let acct = account_id(&db).await;
1968 let now = now_secs();
1969
1970 let live = expiring_order(&db, acct, &["live.example.com"], Some(now + DAY)).await;
1971
1972 let mut revoked =
1974 expiring_order(&db, acct, &["revoked.example.com"], Some(now + DAY)).await;
1975 revoked.revoke(Some(1), &db).await.unwrap();
1976
1977 expiring_order(&db, acct, &["old.example.com"], None).await;
1980
1981 let broken = expiring_order(&db, acct, &["broken.example.com"], None).await;
1984 Order::set_cert_not_after(broken.id, -1, &db).await.unwrap();
1985
1986 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
1987 .await
1988 .unwrap();
1989 let ids: Vec<String> = page.iter().map(|order| order.id.to_string()).collect();
1990 assert_eq!(ids, vec![live.id.to_string()]);
1991 assert_eq!(total, 1);
1992 }
1993
1994 #[tokio::test]
1998 async fn find_expiring_reports_the_unpaged_total() {
1999 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2000 let acct = account_id(&db).await;
2001 let now = now_secs();
2002 for index in 0..5 {
2003 let name = format!("host-{index}.example.com");
2004 expiring_order(&db, acct, &[name.as_str()], Some(now + DAY)).await;
2005 }
2006
2007 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 2, 0, &db)
2008 .await
2009 .unwrap();
2010 assert_eq!(page.len(), 2);
2011 assert_eq!(total, 5);
2012 }
2013
2014 #[tokio::test]
2017 async fn find_expiring_scopes_by_profile() {
2018 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2019 let acct = account_id(&db).await;
2020 let now = now_secs();
2021 expiring_order(&db, acct, &["a.example.com"], Some(now + DAY)).await;
2022
2023 let (page, total) = Order::find_expiring(Some("other"), now + 14 * DAY, 10, 0, &db)
2024 .await
2025 .unwrap();
2026 assert!(page.is_empty());
2027 assert_eq!(total, 0);
2028 }
2029
2030 #[tokio::test]
2034 async fn find_expiring_unscoped_spans_every_profile() {
2035 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2036 let acct = account_id(&db).await;
2037 let now = now_secs();
2038
2039 let here =
2040 expiring_order_on(&db, "default", acct, &["a.example.com"], Some(now + DAY)).await;
2041 let there =
2042 expiring_order_on(&db, "other", acct, &["b.example.com"], Some(now + 2 * DAY)).await;
2043
2044 let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
2045 .await
2046 .unwrap();
2047 let ids: Vec<String> = page.iter().map(|order| order.id.to_string()).collect();
2048 assert_eq!(ids, vec![here.id.to_string(), there.id.to_string()]);
2049 assert_eq!(total, 2);
2050
2051 let mut revoked =
2054 expiring_order_on(&db, "other", acct, &["c.example.com"], Some(now + DAY)).await;
2055 revoked.revoke(Some(1), &db).await.unwrap();
2056 let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
2057 .await
2058 .unwrap();
2059 assert_eq!(page.len(), 2);
2060 assert_eq!(total, 2);
2061 }
2062
2063 #[tokio::test]
2067 async fn find_expiring_pages_without_overlap_and_keeps_the_unpaged_total() {
2068 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2069 let acct = account_id(&db).await;
2070 let now = now_secs();
2071 for index in 0..5 {
2072 let name = format!("host-{index}.example.com");
2073 expiring_order(&db, acct, &[name.as_str()], Some(now + (index + 1) * DAY)).await;
2076 }
2077
2078 let (first, total) = Order::find_expiring(None, now + 14 * DAY, 2, 0, &db)
2079 .await
2080 .unwrap();
2081 let (second, second_total) = Order::find_expiring(None, now + 14 * DAY, 2, 2, &db)
2082 .await
2083 .unwrap();
2084
2085 assert_eq!(total, 5);
2086 assert_eq!(second_total, 5, "the total is unpaged on every window");
2087 assert_eq!(first.len(), 2);
2088 assert_eq!(second.len(), 2);
2089 let firsts: Vec<String> = first.iter().map(|order| order.id.to_string()).collect();
2090 for order in &second {
2091 assert!(
2092 !firsts.contains(&order.id.to_string()),
2093 "a row must not appear on two pages"
2094 );
2095 }
2096
2097 let (past, _) = Order::find_expiring(None, now + 14 * DAY, 2, 50, &db)
2099 .await
2100 .unwrap();
2101 assert!(past.is_empty());
2102 }
2103
2104 #[tokio::test]
2106 async fn find_unstamped_finds_only_issued_rows_with_no_stamp() {
2107 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2108 let acct = account_id(&db).await;
2109
2110 let unstamped = expiring_order(&db, acct, &["old.example.com"], None).await;
2111 expiring_order(&db, acct, &["new.example.com"], Some(now_secs())).await;
2112 Order::create(
2114 "default",
2115 acct,
2116 vec![Identifier::dns("pending.example.com")],
2117 now_secs() + 3600,
2118 None,
2119 None,
2120 &db,
2121 )
2122 .await
2123 .unwrap();
2124
2125 let rows = Order::find_unstamped("default", 10, &db).await.unwrap();
2126 assert_eq!(rows.len(), 1);
2127 assert_eq!(rows[0].0, unstamped.id);
2128
2129 Order::set_cert_not_after(unstamped.id, -1, &db)
2132 .await
2133 .unwrap();
2134 assert!(
2135 Order::find_unstamped("default", 10, &db)
2136 .await
2137 .unwrap()
2138 .is_empty()
2139 );
2140 }
2141}