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: String,
84 pub profile: String,
89 pub account_id: String,
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 ("account_id = ", self.account_id.as_deref()),
152 ("status = ", self.status.map(OrderStatus::as_str)),
153 ] {
154 if let Some(value) = value {
155 builder
156 .push(separator)
157 .push(column)
158 .push_bind(value.to_string());
159 separator = " AND ";
160 }
161 }
162 }
163}
164
165pub(crate) fn rfc3339(secs: i64) -> String {
170 OffsetDateTime::from_unix_timestamp(secs)
171 .ok()
172 .and_then(|dt| dt.format(&Rfc3339).ok())
173 .unwrap_or_default()
174}
175
176macro_rules! columns {
186 () => {
187 "id, profile, account_id, status, identifiers, expires, not_before, not_after, \
188 error, certificate, replaces, cert_serial, cert_pubkey, cert_not_after, \
189 revoked_at, revocation_reason, created_at, created_ip, created_ptr"
190 };
191}
192
193pub const UNPARSABLE_NOT_AFTER: i64 = -1;
202
203fn push_expiring_predicates(
214 profile: Option<&str>,
215 before: i64,
216 builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>,
217) {
218 builder.push(" FROM orders WHERE certificate IS NOT NULL AND revoked_at IS NULL");
219 builder.push(" AND cert_not_after >= 0 AND cert_not_after <= ");
220 builder.push_bind(before);
221 if let Some(profile) = profile {
222 builder.push(" AND profile = ");
223 builder.push_bind(profile.to_string());
224 }
225}
226
227impl Order {
228 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
229 let identifiers_json: String = row.try_get("identifiers")?;
230 let identifiers: Vec<Identifier> = serde_json::from_str(&identifiers_json)
231 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
232
233 let error_json: Option<String> = row.try_get("error")?;
234 let error: Option<Value> = match error_json {
235 Some(text) => {
236 Some(serde_json::from_str(&text).map_err(|e| sqlx::Error::Decode(Box::new(e)))?)
237 }
238 None => None,
239 };
240
241 Ok(Order {
242 id: row.try_get("id")?,
243 profile: row.try_get("profile")?,
244 account_id: row.try_get("account_id")?,
245 status: status::from_column(row.try_get::<&str, _>("status")?)?,
246 identifiers,
247 expires: row.try_get("expires")?,
248 not_before: row.try_get("not_before")?,
249 not_after: row.try_get("not_after")?,
250 error,
251 certificate: row.try_get("certificate")?,
252 replaces: row.try_get("replaces")?,
253 cert_serial: row.try_get("cert_serial")?,
254 cert_pubkey: row.try_get("cert_pubkey")?,
255 cert_not_after: row.try_get("cert_not_after")?,
256 revoked_at: row.try_get("revoked_at")?,
257 revocation_reason: row.try_get("revocation_reason")?,
258 created_at: row.try_get("created_at")?,
259 created_ip: row.try_get("created_ip")?,
260 created_ptr: row.try_get("created_ptr")?,
261 })
262 }
263
264 pub(crate) fn new(
267 profile: &str,
268 account_id: &str,
269 identifiers: Vec<Identifier>,
270 expires: i64,
271 not_before: Option<i64>,
272 not_after: Option<i64>,
273 ) -> Order {
274 Order {
275 id: Uuid::new_v4().to_string(),
276 profile: profile.to_string(),
277 account_id: account_id.to_string(),
278 status: OrderStatus::Pending,
279 identifiers,
280 expires,
281 not_before,
282 not_after,
283 error: None,
284 certificate: None,
285 replaces: None,
288 cert_serial: None,
289 cert_pubkey: None,
290 cert_not_after: None,
291 revoked_at: None,
292 revocation_reason: None,
293 created_at: now_secs(),
294 created_ip: None,
300 created_ptr: None,
301 }
302 }
303
304 #[must_use]
311 pub(crate) fn with_client(mut self, client: &crate::audit::ClientContext) -> Order {
312 self.created_ip = client.ip.clone();
313 self.created_ptr = client.ptr.clone();
314 self
315 }
316
317 pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
324 where
325 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
326 {
327 let identifiers_json = serde_json::to_string(&self.identifiers)
329 .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
330
331 debug!(event = "db_order_create_started", outcome = "progress", order_id = ?self.id, profile = %self.profile, account_id = ?self.account_id);
332 sqlx::query(
333 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, not_before, not_after, error, certificate, replaces, created_at, created_ip, created_ptr) \
334 VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?);",
335 )
336 .bind(&self.id)
337 .bind(&self.profile)
338 .bind(&self.account_id)
339 .bind(self.status.as_str())
340 .bind(identifiers_json)
341 .bind(self.expires)
342 .bind(self.not_before)
343 .bind(self.not_after)
344 .bind(&self.replaces)
345 .bind(self.created_at)
346 .bind(&self.created_ip)
347 .bind(&self.created_ptr)
348 .execute(executor)
349 .await?;
350
351 debug!(event = "db_order_created", outcome = "success", order_id = ?self.id, account_id = ?self.account_id);
352 Ok(())
353 }
354
355 pub async fn create(
358 profile: &str,
359 account_id: &str,
360 identifiers: Vec<Identifier>,
361 expires: i64,
362 not_before: Option<i64>,
363 not_after: Option<i64>,
364 database: &Database,
365 ) -> Result<Order, sqlx::Error> {
366 let order = Order::new(
367 profile,
368 account_id,
369 identifiers,
370 expires,
371 not_before,
372 not_after,
373 );
374 order.insert(&database.pool).await?;
375 Ok(order)
376 }
377
378 pub async fn find_by_id(id: &str, database: &Database) -> Result<Option<Order>, sqlx::Error> {
379 debug!(event = "db_order_find_by_id_started", outcome = "progress", order_id = ?id);
380 let row = sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE id = ?;"))
381 .bind(id)
382 .fetch_optional(&database.pool)
383 .await?;
384
385 let result = row.map(Order::from_row).transpose()?;
386 if result.is_some() {
387 info!(event = "db_order_found_by_id", outcome = "success", order_id = ?id);
388 } else {
389 debug!(event = "db_order_not_found_by_id", outcome = "failure", order_id = ?id);
390 }
391 Ok(result)
392 }
393
394 pub async fn find_by_account(
401 account_id: &str,
402 database: &Database,
403 ) -> Result<Vec<Order>, sqlx::Error> {
404 debug!(event = "db_order_find_by_account_started", outcome = "progress", account_id = ?account_id);
405 let rows = sqlx::query(concat!(
406 "SELECT ",
407 columns!(),
408 " FROM orders WHERE account_id = ? ORDER BY created_at DESC;"
409 ))
410 .bind(account_id)
411 .fetch_all(&database.pool)
412 .await?;
413
414 rows.into_iter().map(Order::from_row).collect()
415 }
416
417 pub async fn find_active_by_account(
430 account_id: &str,
431 database: &Database,
432 ) -> Result<Vec<Order>, sqlx::Error> {
433 debug!(event = "db_order_find_active_by_account_started", outcome = "progress", account_id = ?account_id);
434 let rows =
435 sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE account_id = ? AND status != 'invalid' AND (status = 'valid' OR expires > ?) ORDER BY created_at DESC;"))
436 .bind(account_id)
437 .bind(now_secs())
438 .fetch_all(&database.pool)
439 .await?;
440
441 rows.into_iter().map(Order::from_row).collect()
442 }
443
444 pub async fn search(
464 query: &OrderQuery,
465 database: &Database,
466 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
467 debug!(event = "db_order_search_started",
468 outcome = "progress",
469 profile = ?query.profile,
470 account_id = ?query.account_id,
471 status = ?query.status,
472 limit = query.limit,
473 offset = query.offset);
474
475 let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!(), " FROM orders"));
476 query.push_predicates(&mut page);
477 page.push(" ORDER BY created_at DESC, id DESC LIMIT ");
481 page.push_bind(query.limit);
482 page.push(" OFFSET ");
483 page.push_bind(query.offset);
484
485 let rows = page.build().fetch_all(&database.pool).await?;
486 let orders: Vec<Order> = rows
487 .into_iter()
488 .map(Order::from_row)
489 .collect::<Result<_, _>>()?;
490
491 let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*) FROM orders");
492 query.push_predicates(&mut count);
493 let total: i64 = count
494 .build()
495 .fetch_one(&database.pool)
496 .await?
497 .try_get::<i64, _>(0)?;
498
499 Ok((orders, total))
500 }
501
502 pub async fn cleanup(
519 profile: &str,
520 cutoff: i64,
521 database: &Database,
522 ) -> Result<u64, sqlx::Error> {
523 debug!(event = "db_order_cleanup_started", outcome = "progress", profile = %profile, cutoff = cutoff);
524 let removed = sqlx::query(
525 "DELETE FROM orders WHERE profile = ? AND status != 'valid' AND expires < ?;",
526 )
527 .bind(profile)
528 .bind(cutoff)
529 .execute(&database.pool)
530 .await?
531 .rows_affected();
532
533 debug!(event = "db_order_cleanup_completed", outcome = "success", profile = %profile, rows_removed = removed);
534 Ok(removed)
535 }
536
537 pub async fn count_by_account(
543 account_id: &str,
544 database: &Database,
545 ) -> Result<i64, sqlx::Error> {
546 let row = sqlx::query("SELECT COUNT(*) FROM orders WHERE account_id = ?;")
547 .bind(account_id)
548 .fetch_one(&database.pool)
549 .await?;
550 row.try_get::<i64, _>(0)
551 }
552
553 pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
556 debug!(event = "db_order_delete_started", outcome = "progress", order_id = ?id);
557 let result = sqlx::query("DELETE FROM orders WHERE id = ?;")
558 .bind(id)
559 .execute(&database.pool)
560 .await?;
561
562 let deleted = result.rows_affected() > 0;
563 if deleted {
564 info!(event = "db_order_deleted", outcome = "success", order_id = ?id);
565 } else {
566 debug!(event = "db_order_delete_missing", outcome = "success", order_id = ?id);
567 }
568 Ok(deleted)
569 }
570
571 pub async fn finalize(
589 &mut self,
590 chain: String,
591 cert_serial: String,
592 cert_pubkey: Vec<u8>,
593 cert_not_after: Option<i64>,
594 database: &Database,
595 ) -> Result<(), sqlx::Error> {
596 debug!(event = "db_order_finalize_started", outcome = "progress", order_id = ?self.id);
597 sqlx::query(
598 "UPDATE orders SET certificate = ?, cert_serial = ?, cert_pubkey = ?, \
599 cert_not_after = ?, status = 'valid' WHERE id = ?;",
600 )
601 .bind(&chain)
602 .bind(&cert_serial)
603 .bind(&cert_pubkey)
604 .bind(cert_not_after)
605 .bind(&self.id)
606 .execute(&database.pool)
607 .await?;
608
609 self.certificate = Some(chain);
610 self.cert_serial = Some(cert_serial);
611 self.cert_pubkey = Some(cert_pubkey);
612 self.cert_not_after = cert_not_after;
613 self.status = OrderStatus::Valid;
614 debug!(event = "db_order_finalized", outcome = "success", order_id = ?self.id);
615 Ok(())
616 }
617
618 pub async fn find_expiring(
657 profile: Option<&str>,
658 before: i64,
659 limit: i64,
660 offset: i64,
661 database: &Database,
662 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
663 debug!(
664 event = "db_order_find_expiring_started",
665 outcome = "progress",
666 profile = ?profile,
667 before,
668 limit,
669 offset
670 );
671 let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!()));
672 push_expiring_predicates(profile, before, &mut page);
673 page.push(" ORDER BY cert_not_after ASC, id ASC LIMIT ");
674 page.push_bind(limit);
675 page.push(" OFFSET ");
676 page.push_bind(offset);
677
678 let rows = page.build().fetch_all(&database.pool).await?;
679 let orders: Vec<Order> = rows
680 .into_iter()
681 .map(Order::from_row)
682 .collect::<Result<_, _>>()?;
683
684 let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*)");
685 push_expiring_predicates(profile, before, &mut count);
686 let total: i64 = count
687 .build()
688 .fetch_one(&database.pool)
689 .await?
690 .try_get::<i64, _>(0)?;
691
692 Ok((orders, total))
693 }
694
695 pub async fn find_unstamped(
704 profile: &str,
705 limit: i64,
706 database: &Database,
707 ) -> Result<Vec<(String, String)>, sqlx::Error> {
708 let rows = sqlx::query(
709 "SELECT id, certificate FROM orders WHERE profile = ? \
710 AND certificate IS NOT NULL AND cert_not_after IS NULL LIMIT ?;",
711 )
712 .bind(profile)
713 .bind(limit)
714 .fetch_all(&database.pool)
715 .await?;
716
717 rows.into_iter()
718 .map(|row| Ok((row.try_get("id")?, row.try_get("certificate")?)))
719 .collect()
720 }
721
722 pub async fn set_cert_not_after(
730 id: &str,
731 cert_not_after: i64,
732 database: &Database,
733 ) -> Result<(), sqlx::Error> {
734 sqlx::query("UPDATE orders SET cert_not_after = ? WHERE id = ?;")
735 .bind(cert_not_after)
736 .bind(id)
737 .execute(&database.pool)
738 .await?;
739 Ok(())
740 }
741
742 pub async fn find_by_cert_serial(
754 profile: &str,
755 serial: &str,
756 database: &Database,
757 ) -> Result<Option<Order>, sqlx::Error> {
758 debug!(event = "db_order_find_by_cert_serial_started", outcome = "progress", profile = %profile, cert_serial = ?serial);
759 let row = sqlx::query(concat!(
760 "SELECT ",
761 columns!(),
762 " FROM orders WHERE profile = ? AND cert_serial = ?;"
763 ))
764 .bind(profile)
765 .bind(serial)
766 .fetch_optional(&database.pool)
767 .await?;
768
769 let result = row.map(Order::from_row).transpose()?;
770 if result.is_some() {
771 info!(event = "db_order_found_by_cert_serial", outcome = "success", cert_serial = ?serial);
772 } else {
773 debug!(event = "db_order_not_found_by_cert_serial", outcome = "failure", cert_serial = ?serial);
774 }
775 Ok(result)
776 }
777
778 pub async fn find_by_replaces(
787 profile: &str,
788 cert_id: &str,
789 database: &Database,
790 ) -> Result<Option<Order>, sqlx::Error> {
791 debug!(event = "db_order_find_by_replaces_started", outcome = "progress", profile = %profile, replaces = %cert_id);
792 let row = sqlx::query(concat!(
793 "SELECT ",
794 columns!(),
795 " FROM orders WHERE profile = ? AND replaces = ? AND status != 'invalid' LIMIT 1;"
796 ))
797 .bind(profile)
798 .bind(cert_id)
799 .fetch_optional(&database.pool)
800 .await?;
801
802 row.map(Order::from_row).transpose()
803 }
804
805 pub async fn revoke(
811 &mut self,
812 reason: Option<i64>,
813 database: &Database,
814 ) -> Result<(), sqlx::Error> {
815 let now = now_secs();
816 debug!(event = "db_order_revoke_started", outcome = "progress", order_id = ?self.id, reason = ?reason);
817 sqlx::query("UPDATE orders SET revoked_at = ?, revocation_reason = ? WHERE id = ?;")
818 .bind(now)
819 .bind(reason)
820 .bind(&self.id)
821 .execute(&database.pool)
822 .await?;
823
824 self.revoked_at = Some(now);
825 self.revocation_reason = reason;
826 info!(event = "db_order_revoked", outcome = "success", order_id = ?self.id, reason = ?reason);
827 Ok(())
828 }
829
830 pub(crate) async fn set_invalid<'e, E>(
841 id: &str,
842 error: &Value,
843 executor: E,
844 ) -> Result<(), sqlx::Error>
845 where
846 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
847 {
848 sqlx::query("UPDATE orders SET error = ?, status = 'invalid' WHERE id = ?;")
850 .bind(error.to_string())
851 .bind(id)
852 .execute(executor)
853 .await?;
854 Ok(())
855 }
856
857 pub(crate) async fn set_ready<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
859 where
860 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
861 {
862 sqlx::query("UPDATE orders SET status = 'ready' WHERE id = ?;")
863 .bind(id)
864 .execute(executor)
865 .await?;
866 Ok(())
867 }
868
869 pub(crate) async fn set_pending<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
871 where
872 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
873 {
874 sqlx::query("UPDATE orders SET status = 'pending' WHERE id = ?;")
875 .bind(id)
876 .execute(executor)
877 .await?;
878 Ok(())
879 }
880
881 pub async fn mark_invalid(
882 &mut self,
883 error: Value,
884 database: &Database,
885 ) -> Result<(), sqlx::Error> {
886 debug!(event = "db_order_mark_invalid_started", outcome = "progress", order_id = ?self.id);
887 Self::set_invalid(&self.id, &error, &database.pool).await?;
888
889 self.error = Some(error);
890 self.status = OrderStatus::Invalid;
891 info!(event = "db_order_marked_invalid", outcome = "failure", order_id = ?self.id);
892 Ok(())
893 }
894
895 pub async fn mark_ready(&mut self, database: &Database) -> Result<(), sqlx::Error> {
899 debug!(event = "db_order_mark_ready_started", outcome = "progress", order_id = ?self.id);
900 Self::set_ready(&self.id, &database.pool).await?;
901
902 self.status = OrderStatus::Ready;
903 info!(event = "db_order_marked_ready", outcome = "success", order_id = ?self.id);
904 Ok(())
905 }
906
907 pub async fn mark_pending(&mut self, database: &Database) -> Result<(), sqlx::Error> {
919 debug!(event = "db_order_mark_pending_started", outcome = "progress", order_id = ?self.id);
920 Self::set_pending(&self.id, &database.pool).await?;
921
922 self.status = OrderStatus::Pending;
923 info!(event = "db_order_marked_pending", outcome = "success", order_id = ?self.id);
924 Ok(())
925 }
926
927 pub async fn claim_for_finalize(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
952 debug!(event = "db_order_mark_processing_started", outcome = "progress", order_id = ?self.id);
953 let claimed = sqlx::query(
954 "UPDATE orders SET status = 'processing' WHERE id = ? AND status = 'ready';",
955 )
956 .bind(&self.id)
957 .execute(&database.pool)
958 .await?
959 .rows_affected()
960 == 1;
961
962 if !claimed {
963 debug!(event = "db_order_finalize_claim_refused", outcome = "failure", order_id = ?self.id);
964 return Ok(false);
965 }
966
967 self.status = OrderStatus::Processing;
968 info!(event = "db_order_marked_processing", outcome = "success", order_id = ?self.id);
969 Ok(true)
970 }
971
972 pub async fn release_finalize_claim(&mut self, database: &Database) -> Result<(), sqlx::Error> {
985 let released = sqlx::query(
986 "UPDATE orders SET status = 'ready' WHERE id = ? AND status = 'processing';",
987 )
988 .bind(&self.id)
989 .execute(&database.pool)
990 .await?
991 .rows_affected()
992 == 1;
993
994 if released {
995 self.status = OrderStatus::Ready;
996 }
997 debug!(event = "db_order_finalize_claim_released", outcome = "success", order_id = ?self.id, released = released);
998 Ok(())
999 }
1000
1001 #[must_use]
1006 pub fn to_json(&self, base_url: &str, authz_ids: &[String]) -> Value {
1007 let mut object = serde_json::Map::new();
1008 object.insert(
1009 "status".to_string(),
1010 Value::String(self.status.as_str().to_string()),
1011 );
1012 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
1013 object.insert(
1014 "identifiers".to_string(),
1015 serde_json::to_value(&self.identifiers).expect("Identifier is always serializable"),
1016 );
1017 if let Some(nb) = self.not_before {
1018 object.insert("notBefore".to_string(), Value::String(rfc3339(nb)));
1019 }
1020 if let Some(na) = self.not_after {
1021 object.insert("notAfter".to_string(), Value::String(rfc3339(na)));
1022 }
1023 let authorizations: Vec<Value> = authz_ids
1024 .iter()
1025 .map(|id| Value::String(format!("{base_url}/authz/{id}")))
1026 .collect();
1027 object.insert("authorizations".to_string(), Value::Array(authorizations));
1028 object.insert(
1029 "finalize".to_string(),
1030 Value::String(format!("{base_url}/order/{}/finalize", self.id)),
1031 );
1032 if self.status == OrderStatus::Valid {
1033 object.insert(
1034 "certificate".to_string(),
1035 Value::String(format!("{base_url}/certificate/{}", self.id)),
1036 );
1037 }
1038 if let Some(ref error) = self.error {
1039 object.insert("error".to_string(), error.clone());
1040 }
1041 if let Some(ref replaces) = self.replaces {
1045 object.insert("replaces".to_string(), Value::String(replaces.clone()));
1046 }
1047 Value::Object(object)
1048 }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053 use super::*;
1054 use crate::audit::ClientContext;
1055
1056 #[tokio::test]
1060 async fn with_client_persists_and_an_order_without_one_stays_null() {
1061 let db = std::sync::Arc::new(Database::connect_in_memory().await.unwrap());
1062 let account = account_id(&db).await;
1063
1064 let stamped = Order::new(
1065 "default",
1066 &account,
1067 vec![Identifier::dns("a.example.com")],
1068 0,
1069 None,
1070 None,
1071 )
1072 .with_client(&ClientContext {
1073 ip: Some("203.0.113.7".to_string()),
1074 ptr: Some("host.example.com".to_string()),
1075 user_agent: Some("lego".to_string()),
1076 request_id: Some("req-1".to_string()),
1077 });
1078 stamped.insert(&db.pool).await.unwrap();
1079 let reloaded = Order::find_by_id(&stamped.id, &db).await.unwrap().unwrap();
1080 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
1081 assert_eq!(reloaded.created_ptr.as_deref(), Some("host.example.com"));
1082
1083 let bare = Order::new(
1084 "default",
1085 &account,
1086 vec![Identifier::dns("b.example.com")],
1087 0,
1088 None,
1089 None,
1090 );
1091 bare.insert(&db.pool).await.unwrap();
1092 let reloaded = Order::find_by_id(&bare.id, &db).await.unwrap().unwrap();
1093 assert_eq!(reloaded.created_ip, None);
1094 assert_eq!(reloaded.created_ptr, None);
1095
1096 let json = reloaded.to_json("http://localhost:3000", &[]);
1099 let object = json.as_object().unwrap();
1100 assert!(!object.contains_key("createdIp"));
1101 assert!(!object.contains_key("createdPtr"));
1102 assert!(
1103 !stamped
1104 .to_json("http://localhost:3000", &[])
1105 .to_string()
1106 .contains("203.0.113.7")
1107 );
1108 }
1109
1110 use crate::testutil::account_id;
1111 use serde_json::json;
1112 use std::sync::Arc;
1113
1114 #[tokio::test]
1115 async fn create_then_find_by_id_round_trip() {
1116 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1117 let acct = account_id(&db).await;
1118
1119 let created = Order::create(
1120 "default",
1121 &acct,
1122 vec![Identifier::dns("example.com")],
1123 now_secs() + 3600,
1124 None,
1125 None,
1126 &db,
1127 )
1128 .await
1129 .unwrap();
1130 assert_eq!(created.status, OrderStatus::Pending);
1131
1132 let found = Order::find_by_id(&created.id, &db).await.unwrap().unwrap();
1133 assert_eq!(found.account_id, acct);
1134 assert_eq!(found.identifiers, vec![Identifier::dns("example.com")]);
1135 assert!(found.certificate.is_none());
1136 }
1137
1138 #[tokio::test]
1139 async fn find_by_account_lists_all() {
1140 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1141 let acct = account_id(&db).await;
1142
1143 Order::create(
1144 "default",
1145 &acct,
1146 vec![Identifier::dns("a.example")],
1147 now_secs() + 3600,
1148 None,
1149 None,
1150 &db,
1151 )
1152 .await
1153 .unwrap();
1154 Order::create(
1155 "default",
1156 &acct,
1157 vec![Identifier::dns("b.example")],
1158 now_secs() + 3600,
1159 None,
1160 None,
1161 &db,
1162 )
1163 .await
1164 .unwrap();
1165
1166 let orders = Order::find_by_account(&acct, &db).await.unwrap();
1167 assert_eq!(orders.len(), 2);
1168 }
1169
1170 #[tokio::test]
1171 async fn absent_lookup_returns_none() {
1172 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1173 assert!(Order::find_by_id("nope", &db).await.unwrap().is_none());
1174 }
1175
1176 #[tokio::test]
1177 async fn to_json_shape_when_pending() {
1178 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1179 let acct = account_id(&db).await;
1180
1181 let order = Order::create(
1182 "default",
1183 &acct,
1184 vec![Identifier::dns("example.com")],
1185 now_secs() + 3600,
1186 None,
1187 None,
1188 &db,
1189 )
1190 .await
1191 .unwrap();
1192
1193 let authz_ids = vec!["authz-1".to_string()];
1194 let json = order.to_json("http://localhost:3000", &authz_ids);
1195 assert_eq!(json["status"], "pending");
1196 assert_eq!(
1197 json["authorizations"],
1198 json!(["http://localhost:3000/authz/authz-1"])
1199 );
1200 assert_eq!(
1201 json["finalize"],
1202 format!("http://localhost:3000/order/{}/finalize", order.id)
1203 );
1204 assert_eq!(
1205 json["identifiers"],
1206 json!([{"type": "dns", "value": "example.com"}])
1207 );
1208 assert!(json.get("certificate").is_none());
1210 assert!(json.get("notBefore").is_none());
1211 assert!(json.get("notAfter").is_none());
1212 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
1214 }
1215
1216 #[tokio::test]
1217 async fn to_json_includes_optional_fields() {
1218 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1219 let acct = account_id(&db).await;
1220
1221 let order = Order::create(
1222 "default",
1223 &acct,
1224 vec![Identifier::dns("example.com")],
1225 now_secs() + 3600,
1226 Some(now_secs()),
1227 Some(now_secs() + 7200),
1228 &db,
1229 )
1230 .await
1231 .unwrap();
1232
1233 let json = order.to_json("http://localhost:3000", &[]);
1234 assert!(json["notBefore"].as_str().unwrap().ends_with('Z'));
1235 assert!(json["notAfter"].as_str().unwrap().ends_with('Z'));
1236 }
1237
1238 #[tokio::test]
1239 async fn finalize_persists_and_syncs() {
1240 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1241 let acct = account_id(&db).await;
1242
1243 let mut order = Order::create(
1244 "default",
1245 &acct,
1246 vec![Identifier::dns("example.com")],
1247 now_secs() + 3600,
1248 None,
1249 None,
1250 &db,
1251 )
1252 .await
1253 .unwrap();
1254
1255 order
1256 .finalize(
1257 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1258 "aabbcc".to_string(),
1259 vec![1, 2, 3],
1260 Some(now_secs() + 90 * 24 * 60 * 60),
1261 &db,
1262 )
1263 .await
1264 .unwrap();
1265
1266 assert_eq!(order.status, OrderStatus::Valid);
1268 assert!(order.certificate.is_some());
1269 assert_eq!(order.cert_serial.as_deref(), Some("aabbcc"));
1270 assert_eq!(order.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1271 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1273 assert_eq!(reloaded.status, OrderStatus::Valid);
1274 assert_eq!(reloaded.cert_serial.as_deref(), Some("aabbcc"));
1275 assert_eq!(reloaded.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1276 assert!(reloaded.cert_not_after.is_some());
1277 let json = reloaded.to_json("http://localhost:3000", &[]);
1278 assert_eq!(
1279 json["certificate"],
1280 format!("http://localhost:3000/certificate/{}", order.id)
1281 );
1282 }
1283
1284 #[tokio::test]
1287 async fn only_one_caller_can_claim_an_order_for_finalize() {
1288 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1289 let acct = account_id(&db).await;
1290
1291 let mut order = 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 order.mark_ready(&db).await.unwrap();
1303
1304 let mut rival = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1306
1307 assert!(order.claim_for_finalize(&db).await.unwrap());
1308 assert_eq!(order.status, OrderStatus::Processing);
1309
1310 assert!(!rival.claim_for_finalize(&db).await.unwrap());
1313 assert_eq!(rival.status, OrderStatus::Ready);
1314
1315 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1316 assert_eq!(reloaded.status, OrderStatus::Processing);
1317 }
1318
1319 #[tokio::test]
1323 async fn an_order_that_is_not_ready_cannot_be_claimed() {
1324 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1325 let acct = account_id(&db).await;
1326
1327 for prepare in [
1328 None,
1330 Some(OrderStatus::Valid),
1331 Some(OrderStatus::Invalid),
1332 ] {
1333 let mut order = Order::create(
1334 "default",
1335 &acct,
1336 vec![Identifier::dns("example.com")],
1337 now_secs() + 3600,
1338 None,
1339 None,
1340 &db,
1341 )
1342 .await
1343 .unwrap();
1344 match prepare {
1345 None => {}
1346 Some(OrderStatus::Valid) => order
1347 .finalize("chain".to_string(), "aa".to_string(), vec![1], None, &db)
1348 .await
1349 .unwrap(),
1350 Some(_) => order
1351 .mark_invalid(serde_json::json!({}), &db)
1352 .await
1353 .unwrap(),
1354 }
1355 let before = order.status;
1356
1357 assert!(
1358 !order.claim_for_finalize(&db).await.unwrap(),
1359 "claimed an order in {before}"
1360 );
1361 assert_eq!(order.status, before);
1362 }
1363 }
1364
1365 #[tokio::test]
1368 async fn releasing_a_claim_restores_ready_but_never_overrides_a_demotion() {
1369 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1370 let acct = account_id(&db).await;
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 order.mark_ready(&db).await.unwrap();
1384 assert!(order.claim_for_finalize(&db).await.unwrap());
1385
1386 order.release_finalize_claim(&db).await.unwrap();
1387 assert_eq!(order.status, OrderStatus::Ready);
1388 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1389 assert_eq!(reloaded.status, OrderStatus::Ready);
1390
1391 assert!(order.claim_for_finalize(&db).await.unwrap());
1395 order.mark_pending(&db).await.unwrap();
1396 order.release_finalize_claim(&db).await.unwrap();
1397 assert_eq!(order.status, OrderStatus::Pending);
1398 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1399 assert_eq!(reloaded.status, OrderStatus::Pending);
1400 }
1401
1402 async fn finalized_order(db: Arc<Database>, serial: &str) -> Order {
1403 let acct = account_id(&db).await;
1404 let mut order = Order::create(
1405 "default",
1406 &acct,
1407 vec![Identifier::dns("example.com")],
1408 now_secs() + 3600,
1409 None,
1410 None,
1411 &db,
1412 )
1413 .await
1414 .unwrap();
1415 order
1416 .finalize(
1417 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1418 serial.to_string(),
1419 vec![9, 9, 9],
1420 None,
1421 &db,
1422 )
1423 .await
1424 .unwrap();
1425 order
1426 }
1427
1428 #[tokio::test]
1429 async fn find_by_cert_serial_round_trip() {
1430 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1431 let order = finalized_order(db.clone(), "deadbeef").await;
1432
1433 let found = Order::find_by_cert_serial("default", "deadbeef", &db)
1434 .await
1435 .unwrap()
1436 .unwrap();
1437 assert_eq!(found.id, order.id);
1438
1439 assert!(
1440 Order::find_by_cert_serial("default", "unknown", &db)
1441 .await
1442 .unwrap()
1443 .is_none()
1444 );
1445 }
1446
1447 #[tokio::test]
1448 async fn revoke_persists_and_syncs() {
1449 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1450 let mut order = finalized_order(db.clone(), "aa11bb22").await;
1451
1452 order.revoke(Some(1), &db).await.unwrap();
1453
1454 assert!(order.revoked_at.is_some());
1456 assert_eq!(order.revocation_reason, Some(1));
1457 assert_eq!(order.status, OrderStatus::Valid);
1458 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1460 assert!(reloaded.revoked_at.is_some());
1461 assert_eq!(reloaded.revocation_reason, Some(1));
1462 assert_eq!(reloaded.status, OrderStatus::Valid);
1463 }
1464
1465 #[tokio::test]
1466 async fn revoke_with_no_reason_persists_null() {
1467 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1468 let mut order = finalized_order(db.clone(), "cc33dd44").await;
1469
1470 order.revoke(None, &db).await.unwrap();
1471
1472 assert!(order.revoked_at.is_some());
1473 assert!(order.revocation_reason.is_none());
1474 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1475 assert!(reloaded.revocation_reason.is_none());
1476 }
1477
1478 #[tokio::test]
1479 async fn to_json_never_exposes_revocation_state() {
1480 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1481 let mut order = finalized_order(db.clone(), "ee55ff66").await;
1482 order.revoke(Some(1), &db).await.unwrap();
1483
1484 let json = order.to_json("http://localhost:3000", &[]);
1485 assert!(json.get("revokedAt").is_none());
1486 assert!(json.get("revocationReason").is_none());
1487 assert_eq!(json["status"], "valid");
1488 }
1489
1490 #[tokio::test]
1491 async fn mark_invalid_persists_and_syncs() {
1492 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1493 let acct = account_id(&db).await;
1494
1495 let mut order = Order::create(
1496 "default",
1497 &acct,
1498 vec![Identifier::dns("example.com")],
1499 now_secs() + 3600,
1500 None,
1501 None,
1502 &db,
1503 )
1504 .await
1505 .unwrap();
1506
1507 let error = json!({
1508 "type": "urn:ietf:params:acme:error:serverInternal",
1509 "detail": "boom",
1510 "status": 500,
1511 });
1512 order.mark_invalid(error.clone(), &db).await.unwrap();
1513
1514 assert_eq!(order.status, OrderStatus::Invalid);
1516 assert_eq!(order.error, Some(error.clone()));
1517 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1519 assert_eq!(reloaded.status, OrderStatus::Invalid);
1520 let json = reloaded.to_json("http://localhost:3000", &[]);
1521 assert_eq!(json["error"], error);
1522 }
1523
1524 #[tokio::test]
1525 async fn delete_removes_the_row_and_reports_true() {
1526 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1527 let acct = account_id(&db).await;
1528 let order = Order::create(
1529 "default",
1530 &acct,
1531 vec![Identifier::dns("example.com")],
1532 now_secs() + 3600,
1533 None,
1534 None,
1535 &db,
1536 )
1537 .await
1538 .unwrap();
1539
1540 assert!(Order::delete(&order.id, &db).await.unwrap());
1541 assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
1542 }
1543
1544 #[tokio::test]
1545 async fn delete_of_unknown_id_reports_false() {
1546 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1547 assert!(!Order::delete("nope", &db).await.unwrap());
1548 }
1549
1550 #[tokio::test]
1551 async fn delete_cascades_to_authorizations_and_challenges() {
1552 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1553 let acct = account_id(&db).await;
1554 let order = Order::create(
1555 "default",
1556 &acct,
1557 vec![Identifier::dns("example.com")],
1558 now_secs() + 3600,
1559 None,
1560 None,
1561 &db,
1562 )
1563 .await
1564 .unwrap();
1565
1566 let authz = crate::sqlite::authz::Authorization::create(
1567 &order.id,
1568 Identifier::dns("example.com"),
1569 now_secs() + 3600,
1570 &db,
1571 )
1572 .await
1573 .unwrap();
1574 crate::sqlite::authz::Challenge::create(&authz.id, "http-01", &db)
1575 .await
1576 .unwrap();
1577
1578 Order::delete(&order.id, &db).await.unwrap();
1579
1580 assert!(
1581 crate::sqlite::authz::Authorization::find_by_order(&order.id, &db)
1582 .await
1583 .unwrap()
1584 .is_empty()
1585 );
1586 assert!(
1587 crate::sqlite::authz::Challenge::find_by_authz(&authz.id, &db)
1588 .await
1589 .unwrap()
1590 .is_empty()
1591 );
1592 }
1593
1594 async fn seed_orders(
1598 db: &Arc<Database>,
1599 profile: &str,
1600 account_id: &str,
1601 count: usize,
1602 ) -> Vec<String> {
1603 let base = now_secs();
1604 let mut ids = Vec::new();
1605 for index in 0..count {
1606 let order = Order::create(
1607 profile,
1608 account_id,
1609 vec![Identifier::dns(format!("host-{index}.example.com"))],
1610 base + 3600,
1611 None,
1612 None,
1613 db,
1614 )
1615 .await
1616 .unwrap();
1617 sqlx::query("UPDATE orders SET created_at = ? WHERE id = ?;")
1618 .bind(base - index as i64)
1619 .bind(&order.id)
1620 .execute(&db.pool)
1621 .await
1622 .unwrap();
1623 ids.push(order.id);
1624 }
1625 ids
1628 }
1629
1630 fn window(limit: i64, offset: i64) -> OrderQuery {
1631 OrderQuery {
1632 limit,
1633 offset,
1634 ..OrderQuery::default()
1635 }
1636 }
1637
1638 #[tokio::test]
1639 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1640 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1641 let acct = account_id(&db).await;
1642 let ids = seed_orders(&db, "default", &acct, 5).await;
1643
1644 let (page, total) = Order::search(&window(2, 0), &db).await.unwrap();
1645 assert_eq!(total, 5, "the total must ignore the page window");
1646 assert_eq!(
1647 page.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
1648 ids[..2]
1649 );
1650
1651 let (second, total) = Order::search(&window(2, 2), &db).await.unwrap();
1652 assert_eq!(total, 5);
1653 assert_eq!(
1654 second.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
1655 ids[2..4]
1656 );
1657
1658 let (last, _) = Order::search(&window(2, 4), &db).await.unwrap();
1660 assert_eq!(last.len(), 1);
1661 let (beyond, total) = Order::search(&window(2, 99), &db).await.unwrap();
1662 assert!(beyond.is_empty());
1663 assert_eq!(total, 5, "a page past the end still reports the real total");
1664 }
1665
1666 #[tokio::test]
1669 async fn paging_one_row_at_a_time_sees_every_order_exactly_once() {
1670 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1671 let acct = account_id(&db).await;
1672 let mut expected = Vec::new();
1675 for index in 0..4 {
1676 let order = Order::create(
1677 "default",
1678 &acct,
1679 vec![Identifier::dns(format!("same-second-{index}.example.com"))],
1680 now_secs() + 3600,
1681 None,
1682 None,
1683 &db,
1684 )
1685 .await
1686 .unwrap();
1687 expected.push(order.id);
1688 }
1689 expected.sort();
1690
1691 let mut seen = Vec::new();
1692 for offset in 0..4 {
1693 let (page, total) = Order::search(&window(1, offset), &db).await.unwrap();
1694 assert_eq!(total, 4);
1695 assert_eq!(page.len(), 1);
1696 seen.push(page[0].id.clone());
1697 }
1698 seen.sort();
1699 assert_eq!(
1700 seen, expected,
1701 "pages must be disjoint and cover everything"
1702 );
1703 }
1704
1705 #[tokio::test]
1706 async fn search_filters_by_profile_account_and_status_together() {
1707 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1708 let acct = account_id(&db).await;
1709 let (other_account, _) = crate::sqlite::account::Account::find_or_create(
1710 "default",
1711 &[9u8, 9, 9],
1712 vec![],
1713 &ClientContext::default(),
1714 &db,
1715 )
1716 .await
1717 .unwrap();
1718
1719 seed_orders(&db, "default", &acct, 3).await;
1720 seed_orders(&db, "default", &other_account.id, 2).await;
1721 let mut ready = seed_orders(&db, "default", &acct, 1).await;
1722 let ready_id = ready.pop().unwrap();
1723 Order::find_by_id(&ready_id, &db)
1724 .await
1725 .unwrap()
1726 .unwrap()
1727 .mark_ready(&db)
1728 .await
1729 .unwrap();
1730
1731 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1733 assert_eq!(total, 6);
1734
1735 let by_account = OrderQuery {
1737 account_id: Some(acct.clone()),
1738 ..window(50, 0)
1739 };
1740 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1741 assert_eq!(total, 4);
1742 assert!(rows.iter().all(|o| o.account_id == acct));
1743
1744 let by_status = OrderQuery {
1746 status: Some(OrderStatus::Ready),
1747 ..window(50, 0)
1748 };
1749 let (rows, total) = Order::search(&by_status, &db).await.unwrap();
1750 assert_eq!(total, 1);
1751 assert_eq!(rows[0].id, ready_id);
1752
1753 let combined = OrderQuery {
1755 profile: Some("default".to_string()),
1756 account_id: Some(acct.clone()),
1757 status: Some(OrderStatus::Pending),
1758 limit: 50,
1759 offset: 0,
1760 };
1761 let (rows, total) = Order::search(&combined, &db).await.unwrap();
1762 assert_eq!(rows.len(), 3);
1763 assert_eq!(total, 3);
1764
1765 let none = OrderQuery {
1767 profile: Some("no-such-profile".to_string()),
1768 ..window(50, 0)
1769 };
1770 let (rows, total) = Order::search(&none, &db).await.unwrap();
1771 assert!(rows.is_empty());
1772 assert_eq!(total, 0);
1773 }
1774
1775 #[tokio::test]
1776 async fn search_scopes_by_profile() {
1777 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1778 let acct = account_id(&db).await;
1779 seed_orders(&db, "default", &acct, 2).await;
1780 seed_orders(&db, "other", &acct, 3).await;
1781
1782 let scoped = OrderQuery {
1783 profile: Some("other".to_string()),
1784 ..window(50, 0)
1785 };
1786 let (rows, total) = Order::search(&scoped, &db).await.unwrap();
1787 assert_eq!(total, 3);
1788 assert!(rows.iter().all(|o| o.profile == "other"));
1789 }
1790
1791 #[tokio::test]
1799 async fn a_filter_value_is_bound_not_interpolated() {
1800 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1801 let acct = account_id(&db).await;
1802 seed_orders(&db, "default", &acct, 2).await;
1803
1804 for hostile in ["' OR 1=1 --", "default'; DROP TABLE orders; --"] {
1805 let by_profile = OrderQuery {
1806 profile: Some(hostile.to_string()),
1807 ..window(50, 0)
1808 };
1809 let (rows, total) = Order::search(&by_profile, &db).await.unwrap();
1810 assert!(rows.is_empty(), "the value must be compared, not executed");
1811 assert_eq!(total, 0);
1812
1813 let by_account = OrderQuery {
1814 account_id: Some(hostile.to_string()),
1815 ..window(50, 0)
1816 };
1817 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1818 assert!(rows.is_empty(), "the value must be compared, not executed");
1819 assert_eq!(total, 0);
1820 }
1821
1822 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1824 assert_eq!(total, 2);
1825 }
1826
1827 async fn expiring_order(
1831 db: &Database,
1832 account: &str,
1833 names: &[&str],
1834 not_after: Option<i64>,
1835 ) -> Order {
1836 expiring_order_on(db, "default", account, names, not_after).await
1837 }
1838
1839 async fn expiring_order_on(
1841 db: &Database,
1842 profile: &str,
1843 account: &str,
1844 names: &[&str],
1845 not_after: Option<i64>,
1846 ) -> Order {
1847 let mut order = Order::create(
1848 profile,
1849 account,
1850 names.iter().map(|name| Identifier::dns(*name)).collect(),
1851 now_secs() + 3600,
1852 None,
1853 None,
1854 db,
1855 )
1856 .await
1857 .unwrap();
1858 order
1859 .finalize(
1860 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1861 format!("serial-{}", &order.id[..8]),
1862 vec![1],
1863 not_after,
1864 db,
1865 )
1866 .await
1867 .unwrap();
1868 order
1869 }
1870
1871 const DAY: i64 = 24 * 60 * 60;
1872
1873 #[tokio::test]
1876 async fn find_expiring_returns_the_window_soonest_first() {
1877 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1878 let acct = account_id(&db).await;
1879 let now = now_secs();
1880
1881 let far = expiring_order(&db, &acct, &["far.example.com"], Some(now + 60 * DAY)).await;
1882 let soon = expiring_order(&db, &acct, &["soon.example.com"], Some(now + 2 * DAY)).await;
1883 let mid = expiring_order(&db, &acct, &["mid.example.com"], Some(now + 9 * DAY)).await;
1884
1885 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
1886 .await
1887 .unwrap();
1888
1889 let ids: Vec<&str> = page.iter().map(|order| order.id.as_str()).collect();
1890 assert_eq!(ids, vec![soon.id.as_str(), mid.id.as_str()]);
1891 assert_eq!(total, 2);
1892 assert!(
1893 !ids.contains(&far.id.as_str()),
1894 "a certificate outside the window is not expiring yet"
1895 );
1896 }
1897
1898 #[tokio::test]
1900 async fn find_expiring_skips_revoked_unstamped_and_unparsable_rows() {
1901 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1902 let acct = account_id(&db).await;
1903 let now = now_secs();
1904
1905 let live = expiring_order(&db, &acct, &["live.example.com"], Some(now + DAY)).await;
1906
1907 let mut revoked =
1909 expiring_order(&db, &acct, &["revoked.example.com"], Some(now + DAY)).await;
1910 revoked.revoke(Some(1), &db).await.unwrap();
1911
1912 expiring_order(&db, &acct, &["old.example.com"], None).await;
1915
1916 let broken = expiring_order(&db, &acct, &["broken.example.com"], None).await;
1919 Order::set_cert_not_after(&broken.id, -1, &db)
1920 .await
1921 .unwrap();
1922
1923 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
1924 .await
1925 .unwrap();
1926 let ids: Vec<&str> = page.iter().map(|order| order.id.as_str()).collect();
1927 assert_eq!(ids, vec![live.id.as_str()]);
1928 assert_eq!(total, 1);
1929 }
1930
1931 #[tokio::test]
1935 async fn find_expiring_reports_the_unpaged_total() {
1936 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1937 let acct = account_id(&db).await;
1938 let now = now_secs();
1939 for index in 0..5 {
1940 let name = format!("host-{index}.example.com");
1941 expiring_order(&db, &acct, &[name.as_str()], Some(now + DAY)).await;
1942 }
1943
1944 let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 2, 0, &db)
1945 .await
1946 .unwrap();
1947 assert_eq!(page.len(), 2);
1948 assert_eq!(total, 5);
1949 }
1950
1951 #[tokio::test]
1954 async fn find_expiring_scopes_by_profile() {
1955 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1956 let acct = account_id(&db).await;
1957 let now = now_secs();
1958 expiring_order(&db, &acct, &["a.example.com"], Some(now + DAY)).await;
1959
1960 let (page, total) = Order::find_expiring(Some("other"), now + 14 * DAY, 10, 0, &db)
1961 .await
1962 .unwrap();
1963 assert!(page.is_empty());
1964 assert_eq!(total, 0);
1965 }
1966
1967 #[tokio::test]
1971 async fn find_expiring_unscoped_spans_every_profile() {
1972 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1973 let acct = account_id(&db).await;
1974 let now = now_secs();
1975
1976 let here =
1977 expiring_order_on(&db, "default", &acct, &["a.example.com"], Some(now + DAY)).await;
1978 let there =
1979 expiring_order_on(&db, "other", &acct, &["b.example.com"], Some(now + 2 * DAY)).await;
1980
1981 let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
1982 .await
1983 .unwrap();
1984 let ids: Vec<&str> = page.iter().map(|order| order.id.as_str()).collect();
1985 assert_eq!(ids, vec![here.id.as_str(), there.id.as_str()]);
1986 assert_eq!(total, 2);
1987
1988 let mut revoked =
1991 expiring_order_on(&db, "other", &acct, &["c.example.com"], Some(now + DAY)).await;
1992 revoked.revoke(Some(1), &db).await.unwrap();
1993 let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
1994 .await
1995 .unwrap();
1996 assert_eq!(page.len(), 2);
1997 assert_eq!(total, 2);
1998 }
1999
2000 #[tokio::test]
2004 async fn find_expiring_pages_without_overlap_and_keeps_the_unpaged_total() {
2005 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2006 let acct = account_id(&db).await;
2007 let now = now_secs();
2008 for index in 0..5 {
2009 let name = format!("host-{index}.example.com");
2010 expiring_order(&db, &acct, &[name.as_str()], Some(now + (index + 1) * DAY)).await;
2013 }
2014
2015 let (first, total) = Order::find_expiring(None, now + 14 * DAY, 2, 0, &db)
2016 .await
2017 .unwrap();
2018 let (second, second_total) = Order::find_expiring(None, now + 14 * DAY, 2, 2, &db)
2019 .await
2020 .unwrap();
2021
2022 assert_eq!(total, 5);
2023 assert_eq!(second_total, 5, "the total is unpaged on every window");
2024 assert_eq!(first.len(), 2);
2025 assert_eq!(second.len(), 2);
2026 let firsts: Vec<&str> = first.iter().map(|order| order.id.as_str()).collect();
2027 for order in &second {
2028 assert!(
2029 !firsts.contains(&order.id.as_str()),
2030 "a row must not appear on two pages"
2031 );
2032 }
2033
2034 let (past, _) = Order::find_expiring(None, now + 14 * DAY, 2, 50, &db)
2036 .await
2037 .unwrap();
2038 assert!(past.is_empty());
2039 }
2040
2041 #[tokio::test]
2043 async fn find_unstamped_finds_only_issued_rows_with_no_stamp() {
2044 let db = Arc::new(Database::connect_in_memory().await.unwrap());
2045 let acct = account_id(&db).await;
2046
2047 let unstamped = expiring_order(&db, &acct, &["old.example.com"], None).await;
2048 expiring_order(&db, &acct, &["new.example.com"], Some(now_secs())).await;
2049 Order::create(
2051 "default",
2052 &acct,
2053 vec![Identifier::dns("pending.example.com")],
2054 now_secs() + 3600,
2055 None,
2056 None,
2057 &db,
2058 )
2059 .await
2060 .unwrap();
2061
2062 let rows = Order::find_unstamped("default", 10, &db).await.unwrap();
2063 assert_eq!(rows.len(), 1);
2064 assert_eq!(rows[0].0, unstamped.id);
2065
2066 Order::set_cert_not_after(&unstamped.id, -1, &db)
2069 .await
2070 .unwrap();
2071 assert!(
2072 Order::find_unstamped("default", 10, &db)
2073 .await
2074 .unwrap()
2075 .is_empty()
2076 );
2077 }
2078}