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)]
76pub struct Order {
77 pub id: String,
78 pub profile: String,
83 pub account_id: String,
84 pub status: OrderStatus,
85 pub identifiers: Vec<Identifier>,
86 pub expires: i64,
87 pub not_before: Option<i64>,
88 pub not_after: Option<i64>,
89 pub error: Option<Value>,
90 pub certificate: Option<String>,
91 pub replaces: Option<String>,
97 pub cert_serial: Option<String>,
98 pub cert_pubkey: Option<Vec<u8>>,
99 pub revoked_at: Option<i64>,
100 pub revocation_reason: Option<i64>,
101 pub created_at: i64,
102 pub created_ip: Option<String>,
109 pub created_ptr: Option<String>,
110}
111
112#[derive(Debug, Clone, Default)]
118pub struct OrderQuery {
119 pub profile: Option<String>,
120 pub account_id: Option<String>,
121 pub status: Option<OrderStatus>,
122 pub limit: i64,
125 pub offset: i64,
126}
127
128impl OrderQuery {
129 fn push_predicates(&self, builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>) {
135 let mut separator = " WHERE ";
136 for (column, value) in [
140 ("profile = ", self.profile.as_deref()),
141 ("account_id = ", self.account_id.as_deref()),
142 ("status = ", self.status.map(OrderStatus::as_str)),
143 ] {
144 if let Some(value) = value {
145 builder
146 .push(separator)
147 .push(column)
148 .push_bind(value.to_string());
149 separator = " AND ";
150 }
151 }
152 }
153}
154
155pub(crate) fn rfc3339(secs: i64) -> String {
160 OffsetDateTime::from_unix_timestamp(secs)
161 .ok()
162 .and_then(|dt| dt.format(&Rfc3339).ok())
163 .unwrap_or_default()
164}
165
166macro_rules! columns {
176 () => {
177 "id, profile, account_id, status, identifiers, expires, not_before, not_after, \
178 error, certificate, replaces, cert_serial, cert_pubkey, revoked_at, \
179 revocation_reason, created_at, created_ip, created_ptr"
180 };
181}
182
183impl Order {
184 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
185 let identifiers_json: String = row.try_get("identifiers")?;
186 let identifiers: Vec<Identifier> = serde_json::from_str(&identifiers_json)
187 .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
188
189 let error_json: Option<String> = row.try_get("error")?;
190 let error: Option<Value> = match error_json {
191 Some(text) => {
192 Some(serde_json::from_str(&text).map_err(|e| sqlx::Error::Decode(Box::new(e)))?)
193 }
194 None => None,
195 };
196
197 Ok(Order {
198 id: row.try_get("id")?,
199 profile: row.try_get("profile")?,
200 account_id: row.try_get("account_id")?,
201 status: status::from_column(row.try_get::<&str, _>("status")?)?,
202 identifiers,
203 expires: row.try_get("expires")?,
204 not_before: row.try_get("not_before")?,
205 not_after: row.try_get("not_after")?,
206 error,
207 certificate: row.try_get("certificate")?,
208 replaces: row.try_get("replaces")?,
209 cert_serial: row.try_get("cert_serial")?,
210 cert_pubkey: row.try_get("cert_pubkey")?,
211 revoked_at: row.try_get("revoked_at")?,
212 revocation_reason: row.try_get("revocation_reason")?,
213 created_at: row.try_get("created_at")?,
214 created_ip: row.try_get("created_ip")?,
215 created_ptr: row.try_get("created_ptr")?,
216 })
217 }
218
219 pub(crate) fn new(
222 profile: &str,
223 account_id: &str,
224 identifiers: Vec<Identifier>,
225 expires: i64,
226 not_before: Option<i64>,
227 not_after: Option<i64>,
228 ) -> Order {
229 Order {
230 id: Uuid::new_v4().to_string(),
231 profile: profile.to_string(),
232 account_id: account_id.to_string(),
233 status: OrderStatus::Pending,
234 identifiers,
235 expires,
236 not_before,
237 not_after,
238 error: None,
239 certificate: None,
240 replaces: None,
243 cert_serial: None,
244 cert_pubkey: None,
245 revoked_at: None,
246 revocation_reason: None,
247 created_at: now_secs(),
248 created_ip: None,
254 created_ptr: None,
255 }
256 }
257
258 #[must_use]
265 pub(crate) fn with_client(mut self, client: &crate::audit::ClientContext) -> Order {
266 self.created_ip = client.ip.clone();
267 self.created_ptr = client.ptr.clone();
268 self
269 }
270
271 pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
278 where
279 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
280 {
281 let identifiers_json = serde_json::to_string(&self.identifiers)
283 .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
284
285 debug!(event = "db_order_create_started", outcome = "progress", order_id = ?self.id, profile = %self.profile, account_id = ?self.account_id);
286 sqlx::query(
287 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, not_before, not_after, error, certificate, replaces, created_at, created_ip, created_ptr) \
288 VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?);",
289 )
290 .bind(&self.id)
291 .bind(&self.profile)
292 .bind(&self.account_id)
293 .bind(self.status.as_str())
294 .bind(identifiers_json)
295 .bind(self.expires)
296 .bind(self.not_before)
297 .bind(self.not_after)
298 .bind(&self.replaces)
299 .bind(self.created_at)
300 .bind(&self.created_ip)
301 .bind(&self.created_ptr)
302 .execute(executor)
303 .await?;
304
305 debug!(event = "db_order_created", outcome = "success", order_id = ?self.id, account_id = ?self.account_id);
306 Ok(())
307 }
308
309 pub async fn create(
312 profile: &str,
313 account_id: &str,
314 identifiers: Vec<Identifier>,
315 expires: i64,
316 not_before: Option<i64>,
317 not_after: Option<i64>,
318 database: &Database,
319 ) -> Result<Order, sqlx::Error> {
320 let order = Order::new(
321 profile,
322 account_id,
323 identifiers,
324 expires,
325 not_before,
326 not_after,
327 );
328 order.insert(&database.pool).await?;
329 Ok(order)
330 }
331
332 pub async fn find_by_id(id: &str, database: &Database) -> Result<Option<Order>, sqlx::Error> {
333 debug!(event = "db_order_find_by_id_started", outcome = "progress", order_id = ?id);
334 let row = sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE id = ?;"))
335 .bind(id)
336 .fetch_optional(&database.pool)
337 .await?;
338
339 let result = row.map(Order::from_row).transpose()?;
340 if result.is_some() {
341 info!(event = "db_order_found_by_id", outcome = "success", order_id = ?id);
342 } else {
343 debug!(event = "db_order_not_found_by_id", outcome = "failure", order_id = ?id);
344 }
345 Ok(result)
346 }
347
348 pub async fn find_by_account(
355 account_id: &str,
356 database: &Database,
357 ) -> Result<Vec<Order>, sqlx::Error> {
358 debug!(event = "db_order_find_by_account_started", outcome = "progress", account_id = ?account_id);
359 let rows = sqlx::query(concat!(
360 "SELECT ",
361 columns!(),
362 " FROM orders WHERE account_id = ? ORDER BY created_at DESC;"
363 ))
364 .bind(account_id)
365 .fetch_all(&database.pool)
366 .await?;
367
368 rows.into_iter().map(Order::from_row).collect()
369 }
370
371 pub async fn find_active_by_account(
384 account_id: &str,
385 database: &Database,
386 ) -> Result<Vec<Order>, sqlx::Error> {
387 debug!(event = "db_order_find_active_by_account_started", outcome = "progress", account_id = ?account_id);
388 let rows =
389 sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE account_id = ? AND status != 'invalid' AND (status = 'valid' OR expires > ?) ORDER BY created_at DESC;"))
390 .bind(account_id)
391 .bind(now_secs())
392 .fetch_all(&database.pool)
393 .await?;
394
395 rows.into_iter().map(Order::from_row).collect()
396 }
397
398 pub async fn list_all(
405 profile: Option<&str>,
406 database: &Database,
407 ) -> Result<Vec<Order>, sqlx::Error> {
408 debug!(event = "db_order_list_all_started", outcome = "progress", profile = ?profile);
409 let rows = match profile {
410 Some(profile) => {
411 sqlx::query(concat!(
412 "SELECT ",
413 columns!(),
414 " FROM orders WHERE profile = ? ORDER BY created_at ASC;"
415 ))
416 .bind(profile)
417 .fetch_all(&database.pool)
418 .await?
419 }
420 None => {
421 sqlx::query(concat!(
422 "SELECT ",
423 columns!(),
424 " FROM orders ORDER BY created_at ASC;"
425 ))
426 .fetch_all(&database.pool)
427 .await?
428 }
429 };
430
431 rows.into_iter().map(Order::from_row).collect()
432 }
433
434 pub async fn search(
454 query: &OrderQuery,
455 database: &Database,
456 ) -> Result<(Vec<Order>, i64), sqlx::Error> {
457 debug!(event = "db_order_search_started",
458 outcome = "progress",
459 profile = ?query.profile,
460 account_id = ?query.account_id,
461 status = ?query.status,
462 limit = query.limit,
463 offset = query.offset);
464
465 let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!(), " FROM orders"));
466 query.push_predicates(&mut page);
467 page.push(" ORDER BY created_at DESC, id DESC LIMIT ");
471 page.push_bind(query.limit);
472 page.push(" OFFSET ");
473 page.push_bind(query.offset);
474
475 let rows = page.build().fetch_all(&database.pool).await?;
476 let orders: Vec<Order> = rows
477 .into_iter()
478 .map(Order::from_row)
479 .collect::<Result<_, _>>()?;
480
481 let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*) FROM orders");
482 query.push_predicates(&mut count);
483 let total: i64 = count
484 .build()
485 .fetch_one(&database.pool)
486 .await?
487 .try_get::<i64, _>(0)?;
488
489 Ok((orders, total))
490 }
491
492 pub async fn count_by_account(
498 account_id: &str,
499 database: &Database,
500 ) -> Result<i64, sqlx::Error> {
501 let row = sqlx::query("SELECT COUNT(*) FROM orders WHERE account_id = ?;")
502 .bind(account_id)
503 .fetch_one(&database.pool)
504 .await?;
505 row.try_get::<i64, _>(0)
506 }
507
508 pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
511 debug!(event = "db_order_delete_started", outcome = "progress", order_id = ?id);
512 let result = sqlx::query("DELETE FROM orders WHERE id = ?;")
513 .bind(id)
514 .execute(&database.pool)
515 .await?;
516
517 let deleted = result.rows_affected() > 0;
518 if deleted {
519 info!(event = "db_order_deleted", outcome = "success", order_id = ?id);
520 } else {
521 debug!(event = "db_order_delete_missing", outcome = "success", order_id = ?id);
522 }
523 Ok(deleted)
524 }
525
526 pub async fn finalize(
534 &mut self,
535 chain: String,
536 cert_serial: String,
537 cert_pubkey: Vec<u8>,
538 database: &Database,
539 ) -> Result<(), sqlx::Error> {
540 debug!(event = "db_order_finalize_started", outcome = "progress", order_id = ?self.id);
541 sqlx::query(
542 "UPDATE orders SET certificate = ?, cert_serial = ?, cert_pubkey = ?, status = 'valid' WHERE id = ?;",
543 )
544 .bind(&chain)
545 .bind(&cert_serial)
546 .bind(&cert_pubkey)
547 .bind(&self.id)
548 .execute(&database.pool)
549 .await?;
550
551 self.certificate = Some(chain);
552 self.cert_serial = Some(cert_serial);
553 self.cert_pubkey = Some(cert_pubkey);
554 self.status = OrderStatus::Valid;
555 debug!(event = "db_order_finalized", outcome = "success", order_id = ?self.id);
556 Ok(())
557 }
558
559 pub async fn find_by_cert_serial(
571 profile: &str,
572 serial: &str,
573 database: &Database,
574 ) -> Result<Option<Order>, sqlx::Error> {
575 debug!(event = "db_order_find_by_cert_serial_started", outcome = "progress", profile = %profile, cert_serial = ?serial);
576 let row = sqlx::query(concat!(
577 "SELECT ",
578 columns!(),
579 " FROM orders WHERE profile = ? AND cert_serial = ?;"
580 ))
581 .bind(profile)
582 .bind(serial)
583 .fetch_optional(&database.pool)
584 .await?;
585
586 let result = row.map(Order::from_row).transpose()?;
587 if result.is_some() {
588 info!(event = "db_order_found_by_cert_serial", outcome = "success", cert_serial = ?serial);
589 } else {
590 debug!(event = "db_order_not_found_by_cert_serial", outcome = "failure", cert_serial = ?serial);
591 }
592 Ok(result)
593 }
594
595 pub async fn find_by_replaces(
604 profile: &str,
605 cert_id: &str,
606 database: &Database,
607 ) -> Result<Option<Order>, sqlx::Error> {
608 debug!(event = "db_order_find_by_replaces_started", outcome = "progress", profile = %profile, replaces = %cert_id);
609 let row = sqlx::query(concat!(
610 "SELECT ",
611 columns!(),
612 " FROM orders WHERE profile = ? AND replaces = ? AND status != 'invalid' LIMIT 1;"
613 ))
614 .bind(profile)
615 .bind(cert_id)
616 .fetch_optional(&database.pool)
617 .await?;
618
619 row.map(Order::from_row).transpose()
620 }
621
622 pub async fn revoke(
628 &mut self,
629 reason: Option<i64>,
630 database: &Database,
631 ) -> Result<(), sqlx::Error> {
632 let now = now_secs();
633 debug!(event = "db_order_revoke_started", outcome = "progress", order_id = ?self.id, reason = ?reason);
634 sqlx::query("UPDATE orders SET revoked_at = ?, revocation_reason = ? WHERE id = ?;")
635 .bind(now)
636 .bind(reason)
637 .bind(&self.id)
638 .execute(&database.pool)
639 .await?;
640
641 self.revoked_at = Some(now);
642 self.revocation_reason = reason;
643 info!(event = "db_order_revoked", outcome = "success", order_id = ?self.id, reason = ?reason);
644 Ok(())
645 }
646
647 pub(crate) async fn set_invalid<'e, E>(
658 id: &str,
659 error: &Value,
660 executor: E,
661 ) -> Result<(), sqlx::Error>
662 where
663 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
664 {
665 sqlx::query("UPDATE orders SET error = ?, status = 'invalid' WHERE id = ?;")
667 .bind(error.to_string())
668 .bind(id)
669 .execute(executor)
670 .await?;
671 Ok(())
672 }
673
674 pub(crate) async fn set_ready<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
676 where
677 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
678 {
679 sqlx::query("UPDATE orders SET status = 'ready' WHERE id = ?;")
680 .bind(id)
681 .execute(executor)
682 .await?;
683 Ok(())
684 }
685
686 pub(crate) async fn set_pending<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
688 where
689 E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
690 {
691 sqlx::query("UPDATE orders SET status = 'pending' WHERE id = ?;")
692 .bind(id)
693 .execute(executor)
694 .await?;
695 Ok(())
696 }
697
698 pub async fn mark_invalid(
699 &mut self,
700 error: Value,
701 database: &Database,
702 ) -> Result<(), sqlx::Error> {
703 debug!(event = "db_order_mark_invalid_started", outcome = "progress", order_id = ?self.id);
704 Self::set_invalid(&self.id, &error, &database.pool).await?;
705
706 self.error = Some(error);
707 self.status = OrderStatus::Invalid;
708 info!(event = "db_order_marked_invalid", outcome = "failure", order_id = ?self.id);
709 Ok(())
710 }
711
712 pub async fn mark_ready(&mut self, database: &Database) -> Result<(), sqlx::Error> {
716 debug!(event = "db_order_mark_ready_started", outcome = "progress", order_id = ?self.id);
717 Self::set_ready(&self.id, &database.pool).await?;
718
719 self.status = OrderStatus::Ready;
720 info!(event = "db_order_marked_ready", outcome = "success", order_id = ?self.id);
721 Ok(())
722 }
723
724 pub async fn mark_pending(&mut self, database: &Database) -> Result<(), sqlx::Error> {
736 debug!(event = "db_order_mark_pending_started", outcome = "progress", order_id = ?self.id);
737 Self::set_pending(&self.id, &database.pool).await?;
738
739 self.status = OrderStatus::Pending;
740 info!(event = "db_order_marked_pending", outcome = "success", order_id = ?self.id);
741 Ok(())
742 }
743
744 pub async fn claim_for_finalize(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
769 debug!(event = "db_order_mark_processing_started", outcome = "progress", order_id = ?self.id);
770 let claimed = sqlx::query(
771 "UPDATE orders SET status = 'processing' WHERE id = ? AND status = 'ready';",
772 )
773 .bind(&self.id)
774 .execute(&database.pool)
775 .await?
776 .rows_affected()
777 == 1;
778
779 if !claimed {
780 debug!(event = "db_order_finalize_claim_refused", outcome = "failure", order_id = ?self.id);
781 return Ok(false);
782 }
783
784 self.status = OrderStatus::Processing;
785 info!(event = "db_order_marked_processing", outcome = "success", order_id = ?self.id);
786 Ok(true)
787 }
788
789 pub async fn release_finalize_claim(&mut self, database: &Database) -> Result<(), sqlx::Error> {
802 let released = sqlx::query(
803 "UPDATE orders SET status = 'ready' WHERE id = ? AND status = 'processing';",
804 )
805 .bind(&self.id)
806 .execute(&database.pool)
807 .await?
808 .rows_affected()
809 == 1;
810
811 if released {
812 self.status = OrderStatus::Ready;
813 }
814 debug!(event = "db_order_finalize_claim_released", outcome = "success", order_id = ?self.id, released = released);
815 Ok(())
816 }
817
818 #[must_use]
823 pub fn to_json(&self, base_url: &str, authz_ids: &[String]) -> Value {
824 let mut object = serde_json::Map::new();
825 object.insert(
826 "status".to_string(),
827 Value::String(self.status.as_str().to_string()),
828 );
829 object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
830 object.insert(
831 "identifiers".to_string(),
832 serde_json::to_value(&self.identifiers).expect("Identifier is always serializable"),
833 );
834 if let Some(nb) = self.not_before {
835 object.insert("notBefore".to_string(), Value::String(rfc3339(nb)));
836 }
837 if let Some(na) = self.not_after {
838 object.insert("notAfter".to_string(), Value::String(rfc3339(na)));
839 }
840 let authorizations: Vec<Value> = authz_ids
841 .iter()
842 .map(|id| Value::String(format!("{base_url}/authz/{id}")))
843 .collect();
844 object.insert("authorizations".to_string(), Value::Array(authorizations));
845 object.insert(
846 "finalize".to_string(),
847 Value::String(format!("{base_url}/order/{}/finalize", self.id)),
848 );
849 if self.status == OrderStatus::Valid {
850 object.insert(
851 "certificate".to_string(),
852 Value::String(format!("{base_url}/certificate/{}", self.id)),
853 );
854 }
855 if let Some(ref error) = self.error {
856 object.insert("error".to_string(), error.clone());
857 }
858 if let Some(ref replaces) = self.replaces {
862 object.insert("replaces".to_string(), Value::String(replaces.clone()));
863 }
864 Value::Object(object)
865 }
866}
867
868#[cfg(test)]
869mod tests {
870 use super::*;
871 use crate::audit::ClientContext;
872
873 #[tokio::test]
877 async fn with_client_persists_and_an_order_without_one_stays_null() {
878 let db = std::sync::Arc::new(Database::connect_in_memory().await.unwrap());
879 let account = account_id(&db).await;
880
881 let stamped = Order::new(
882 "default",
883 &account,
884 vec![Identifier::dns("a.example.com")],
885 0,
886 None,
887 None,
888 )
889 .with_client(&ClientContext {
890 ip: Some("203.0.113.7".to_string()),
891 ptr: Some("host.example.com".to_string()),
892 user_agent: Some("lego".to_string()),
893 request_id: Some("req-1".to_string()),
894 });
895 stamped.insert(&db.pool).await.unwrap();
896 let reloaded = Order::find_by_id(&stamped.id, &db).await.unwrap().unwrap();
897 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
898 assert_eq!(reloaded.created_ptr.as_deref(), Some("host.example.com"));
899
900 let bare = Order::new(
901 "default",
902 &account,
903 vec![Identifier::dns("b.example.com")],
904 0,
905 None,
906 None,
907 );
908 bare.insert(&db.pool).await.unwrap();
909 let reloaded = Order::find_by_id(&bare.id, &db).await.unwrap().unwrap();
910 assert_eq!(reloaded.created_ip, None);
911 assert_eq!(reloaded.created_ptr, None);
912
913 let json = reloaded.to_json("http://localhost:3000", &[]);
916 let object = json.as_object().unwrap();
917 assert!(!object.contains_key("createdIp"));
918 assert!(!object.contains_key("createdPtr"));
919 assert!(
920 !stamped
921 .to_json("http://localhost:3000", &[])
922 .to_string()
923 .contains("203.0.113.7")
924 );
925 }
926
927 use crate::testutil::account_id;
928 use serde_json::json;
929 use std::sync::Arc;
930
931 #[tokio::test]
932 async fn create_then_find_by_id_round_trip() {
933 let db = Arc::new(Database::connect_in_memory().await.unwrap());
934 let acct = account_id(&db).await;
935
936 let created = Order::create(
937 "default",
938 &acct,
939 vec![Identifier::dns("example.com")],
940 now_secs() + 3600,
941 None,
942 None,
943 &db,
944 )
945 .await
946 .unwrap();
947 assert_eq!(created.status, OrderStatus::Pending);
948
949 let found = Order::find_by_id(&created.id, &db).await.unwrap().unwrap();
950 assert_eq!(found.account_id, acct);
951 assert_eq!(found.identifiers, vec![Identifier::dns("example.com")]);
952 assert!(found.certificate.is_none());
953 }
954
955 #[tokio::test]
956 async fn find_by_account_lists_all() {
957 let db = Arc::new(Database::connect_in_memory().await.unwrap());
958 let acct = account_id(&db).await;
959
960 Order::create(
961 "default",
962 &acct,
963 vec![Identifier::dns("a.example")],
964 now_secs() + 3600,
965 None,
966 None,
967 &db,
968 )
969 .await
970 .unwrap();
971 Order::create(
972 "default",
973 &acct,
974 vec![Identifier::dns("b.example")],
975 now_secs() + 3600,
976 None,
977 None,
978 &db,
979 )
980 .await
981 .unwrap();
982
983 let orders = Order::find_by_account(&acct, &db).await.unwrap();
984 assert_eq!(orders.len(), 2);
985 }
986
987 #[tokio::test]
988 async fn absent_lookup_returns_none() {
989 let db = Arc::new(Database::connect_in_memory().await.unwrap());
990 assert!(Order::find_by_id("nope", &db).await.unwrap().is_none());
991 }
992
993 #[tokio::test]
994 async fn to_json_shape_when_pending() {
995 let db = Arc::new(Database::connect_in_memory().await.unwrap());
996 let acct = account_id(&db).await;
997
998 let order = Order::create(
999 "default",
1000 &acct,
1001 vec![Identifier::dns("example.com")],
1002 now_secs() + 3600,
1003 None,
1004 None,
1005 &db,
1006 )
1007 .await
1008 .unwrap();
1009
1010 let authz_ids = vec!["authz-1".to_string()];
1011 let json = order.to_json("http://localhost:3000", &authz_ids);
1012 assert_eq!(json["status"], "pending");
1013 assert_eq!(
1014 json["authorizations"],
1015 json!(["http://localhost:3000/authz/authz-1"])
1016 );
1017 assert_eq!(
1018 json["finalize"],
1019 format!("http://localhost:3000/order/{}/finalize", order.id)
1020 );
1021 assert_eq!(
1022 json["identifiers"],
1023 json!([{"type": "dns", "value": "example.com"}])
1024 );
1025 assert!(json.get("certificate").is_none());
1027 assert!(json.get("notBefore").is_none());
1028 assert!(json.get("notAfter").is_none());
1029 assert!(json["expires"].as_str().unwrap().ends_with('Z'));
1031 }
1032
1033 #[tokio::test]
1034 async fn to_json_includes_optional_fields() {
1035 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1036 let acct = account_id(&db).await;
1037
1038 let order = Order::create(
1039 "default",
1040 &acct,
1041 vec![Identifier::dns("example.com")],
1042 now_secs() + 3600,
1043 Some(now_secs()),
1044 Some(now_secs() + 7200),
1045 &db,
1046 )
1047 .await
1048 .unwrap();
1049
1050 let json = order.to_json("http://localhost:3000", &[]);
1051 assert!(json["notBefore"].as_str().unwrap().ends_with('Z'));
1052 assert!(json["notAfter"].as_str().unwrap().ends_with('Z'));
1053 }
1054
1055 #[tokio::test]
1056 async fn finalize_persists_and_syncs() {
1057 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1058 let acct = account_id(&db).await;
1059
1060 let mut order = Order::create(
1061 "default",
1062 &acct,
1063 vec![Identifier::dns("example.com")],
1064 now_secs() + 3600,
1065 None,
1066 None,
1067 &db,
1068 )
1069 .await
1070 .unwrap();
1071
1072 order
1073 .finalize(
1074 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1075 "aabbcc".to_string(),
1076 vec![1, 2, 3],
1077 &db,
1078 )
1079 .await
1080 .unwrap();
1081
1082 assert_eq!(order.status, OrderStatus::Valid);
1084 assert!(order.certificate.is_some());
1085 assert_eq!(order.cert_serial.as_deref(), Some("aabbcc"));
1086 assert_eq!(order.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1087 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1089 assert_eq!(reloaded.status, OrderStatus::Valid);
1090 assert_eq!(reloaded.cert_serial.as_deref(), Some("aabbcc"));
1091 assert_eq!(reloaded.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1092 let json = reloaded.to_json("http://localhost:3000", &[]);
1093 assert_eq!(
1094 json["certificate"],
1095 format!("http://localhost:3000/certificate/{}", order.id)
1096 );
1097 }
1098
1099 #[tokio::test]
1102 async fn only_one_caller_can_claim_an_order_for_finalize() {
1103 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1104 let acct = account_id(&db).await;
1105
1106 let mut order = Order::create(
1107 "default",
1108 &acct,
1109 vec![Identifier::dns("example.com")],
1110 now_secs() + 3600,
1111 None,
1112 None,
1113 &db,
1114 )
1115 .await
1116 .unwrap();
1117 order.mark_ready(&db).await.unwrap();
1118
1119 let mut rival = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1121
1122 assert!(order.claim_for_finalize(&db).await.unwrap());
1123 assert_eq!(order.status, OrderStatus::Processing);
1124
1125 assert!(!rival.claim_for_finalize(&db).await.unwrap());
1128 assert_eq!(rival.status, OrderStatus::Ready);
1129
1130 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1131 assert_eq!(reloaded.status, OrderStatus::Processing);
1132 }
1133
1134 #[tokio::test]
1138 async fn an_order_that_is_not_ready_cannot_be_claimed() {
1139 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1140 let acct = account_id(&db).await;
1141
1142 for prepare in [
1143 None,
1145 Some(OrderStatus::Valid),
1146 Some(OrderStatus::Invalid),
1147 ] {
1148 let mut order = Order::create(
1149 "default",
1150 &acct,
1151 vec![Identifier::dns("example.com")],
1152 now_secs() + 3600,
1153 None,
1154 None,
1155 &db,
1156 )
1157 .await
1158 .unwrap();
1159 match prepare {
1160 None => {}
1161 Some(OrderStatus::Valid) => order
1162 .finalize("chain".to_string(), "aa".to_string(), vec![1], &db)
1163 .await
1164 .unwrap(),
1165 Some(_) => order
1166 .mark_invalid(serde_json::json!({}), &db)
1167 .await
1168 .unwrap(),
1169 }
1170 let before = order.status;
1171
1172 assert!(
1173 !order.claim_for_finalize(&db).await.unwrap(),
1174 "claimed an order in {before}"
1175 );
1176 assert_eq!(order.status, before);
1177 }
1178 }
1179
1180 #[tokio::test]
1183 async fn releasing_a_claim_restores_ready_but_never_overrides_a_demotion() {
1184 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1185 let acct = account_id(&db).await;
1186
1187 let mut order = Order::create(
1188 "default",
1189 &acct,
1190 vec![Identifier::dns("example.com")],
1191 now_secs() + 3600,
1192 None,
1193 None,
1194 &db,
1195 )
1196 .await
1197 .unwrap();
1198 order.mark_ready(&db).await.unwrap();
1199 assert!(order.claim_for_finalize(&db).await.unwrap());
1200
1201 order.release_finalize_claim(&db).await.unwrap();
1202 assert_eq!(order.status, OrderStatus::Ready);
1203 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1204 assert_eq!(reloaded.status, OrderStatus::Ready);
1205
1206 assert!(order.claim_for_finalize(&db).await.unwrap());
1210 order.mark_pending(&db).await.unwrap();
1211 order.release_finalize_claim(&db).await.unwrap();
1212 assert_eq!(order.status, OrderStatus::Pending);
1213 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1214 assert_eq!(reloaded.status, OrderStatus::Pending);
1215 }
1216
1217 async fn finalized_order(db: Arc<Database>, serial: &str) -> Order {
1218 let acct = account_id(&db).await;
1219 let mut order = Order::create(
1220 "default",
1221 &acct,
1222 vec![Identifier::dns("example.com")],
1223 now_secs() + 3600,
1224 None,
1225 None,
1226 &db,
1227 )
1228 .await
1229 .unwrap();
1230 order
1231 .finalize(
1232 "-----BEGIN CERTIFICATE-----\n...".to_string(),
1233 serial.to_string(),
1234 vec![9, 9, 9],
1235 &db,
1236 )
1237 .await
1238 .unwrap();
1239 order
1240 }
1241
1242 #[tokio::test]
1243 async fn find_by_cert_serial_round_trip() {
1244 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1245 let order = finalized_order(db.clone(), "deadbeef").await;
1246
1247 let found = Order::find_by_cert_serial("default", "deadbeef", &db)
1248 .await
1249 .unwrap()
1250 .unwrap();
1251 assert_eq!(found.id, order.id);
1252
1253 assert!(
1254 Order::find_by_cert_serial("default", "unknown", &db)
1255 .await
1256 .unwrap()
1257 .is_none()
1258 );
1259 }
1260
1261 #[tokio::test]
1262 async fn revoke_persists_and_syncs() {
1263 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1264 let mut order = finalized_order(db.clone(), "aa11bb22").await;
1265
1266 order.revoke(Some(1), &db).await.unwrap();
1267
1268 assert!(order.revoked_at.is_some());
1270 assert_eq!(order.revocation_reason, Some(1));
1271 assert_eq!(order.status, OrderStatus::Valid);
1272 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1274 assert!(reloaded.revoked_at.is_some());
1275 assert_eq!(reloaded.revocation_reason, Some(1));
1276 assert_eq!(reloaded.status, OrderStatus::Valid);
1277 }
1278
1279 #[tokio::test]
1280 async fn revoke_with_no_reason_persists_null() {
1281 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1282 let mut order = finalized_order(db.clone(), "cc33dd44").await;
1283
1284 order.revoke(None, &db).await.unwrap();
1285
1286 assert!(order.revoked_at.is_some());
1287 assert!(order.revocation_reason.is_none());
1288 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1289 assert!(reloaded.revocation_reason.is_none());
1290 }
1291
1292 #[tokio::test]
1293 async fn to_json_never_exposes_revocation_state() {
1294 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1295 let mut order = finalized_order(db.clone(), "ee55ff66").await;
1296 order.revoke(Some(1), &db).await.unwrap();
1297
1298 let json = order.to_json("http://localhost:3000", &[]);
1299 assert!(json.get("revokedAt").is_none());
1300 assert!(json.get("revocationReason").is_none());
1301 assert_eq!(json["status"], "valid");
1302 }
1303
1304 #[tokio::test]
1305 async fn mark_invalid_persists_and_syncs() {
1306 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1307 let acct = account_id(&db).await;
1308
1309 let mut order = Order::create(
1310 "default",
1311 &acct,
1312 vec![Identifier::dns("example.com")],
1313 now_secs() + 3600,
1314 None,
1315 None,
1316 &db,
1317 )
1318 .await
1319 .unwrap();
1320
1321 let error = json!({
1322 "type": "urn:ietf:params:acme:error:serverInternal",
1323 "detail": "boom",
1324 "status": 500,
1325 });
1326 order.mark_invalid(error.clone(), &db).await.unwrap();
1327
1328 assert_eq!(order.status, OrderStatus::Invalid);
1330 assert_eq!(order.error, Some(error.clone()));
1331 let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1333 assert_eq!(reloaded.status, OrderStatus::Invalid);
1334 let json = reloaded.to_json("http://localhost:3000", &[]);
1335 assert_eq!(json["error"], error);
1336 }
1337
1338 #[tokio::test]
1339 async fn list_all_lists_orders_across_accounts_oldest_first() {
1340 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1341 let acct1 = account_id(&db).await;
1342 let (acct2, _) = crate::sqlite::account::Account::find_or_create(
1343 "default",
1344 &[9u8],
1345 vec![],
1346 &ClientContext::default(),
1347 &db,
1348 )
1349 .await
1350 .unwrap();
1351
1352 let first = Order::create(
1353 "default",
1354 &acct1,
1355 vec![Identifier::dns("a.example")],
1356 now_secs() + 3600,
1357 None,
1358 None,
1359 &db,
1360 )
1361 .await
1362 .unwrap();
1363 let second = Order::create(
1364 "default",
1365 &acct2.id,
1366 vec![Identifier::dns("b.example")],
1367 now_secs() + 3600,
1368 None,
1369 None,
1370 &db,
1371 )
1372 .await
1373 .unwrap();
1374
1375 let all = Order::list_all(None, &db).await.unwrap();
1376 assert_eq!(all.len(), 2);
1377 assert_eq!(all[0].id, first.id);
1378 assert_eq!(all[1].id, second.id);
1379 }
1380
1381 #[tokio::test]
1382 async fn list_all_when_empty_is_empty() {
1383 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1384 assert!(Order::list_all(None, &db).await.unwrap().is_empty());
1385 }
1386
1387 #[tokio::test]
1388 async fn delete_removes_the_row_and_reports_true() {
1389 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1390 let acct = account_id(&db).await;
1391 let order = Order::create(
1392 "default",
1393 &acct,
1394 vec![Identifier::dns("example.com")],
1395 now_secs() + 3600,
1396 None,
1397 None,
1398 &db,
1399 )
1400 .await
1401 .unwrap();
1402
1403 assert!(Order::delete(&order.id, &db).await.unwrap());
1404 assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
1405 }
1406
1407 #[tokio::test]
1408 async fn delete_of_unknown_id_reports_false() {
1409 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1410 assert!(!Order::delete("nope", &db).await.unwrap());
1411 }
1412
1413 #[tokio::test]
1414 async fn delete_cascades_to_authorizations_and_challenges() {
1415 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1416 let acct = account_id(&db).await;
1417 let order = Order::create(
1418 "default",
1419 &acct,
1420 vec![Identifier::dns("example.com")],
1421 now_secs() + 3600,
1422 None,
1423 None,
1424 &db,
1425 )
1426 .await
1427 .unwrap();
1428
1429 let authz = crate::sqlite::authz::Authorization::create(
1430 &order.id,
1431 Identifier::dns("example.com"),
1432 now_secs() + 3600,
1433 &db,
1434 )
1435 .await
1436 .unwrap();
1437 crate::sqlite::authz::Challenge::create(&authz.id, "http-01", &db)
1438 .await
1439 .unwrap();
1440
1441 Order::delete(&order.id, &db).await.unwrap();
1442
1443 assert!(
1444 crate::sqlite::authz::Authorization::find_by_order(&order.id, &db)
1445 .await
1446 .unwrap()
1447 .is_empty()
1448 );
1449 assert!(
1450 crate::sqlite::authz::Challenge::find_by_authz(&authz.id, &db)
1451 .await
1452 .unwrap()
1453 .is_empty()
1454 );
1455 }
1456
1457 async fn seed_orders(
1461 db: &Arc<Database>,
1462 profile: &str,
1463 account_id: &str,
1464 count: usize,
1465 ) -> Vec<String> {
1466 let base = now_secs();
1467 let mut ids = Vec::new();
1468 for index in 0..count {
1469 let order = Order::create(
1470 profile,
1471 account_id,
1472 vec![Identifier::dns(format!("host-{index}.example.com"))],
1473 base + 3600,
1474 None,
1475 None,
1476 db,
1477 )
1478 .await
1479 .unwrap();
1480 sqlx::query("UPDATE orders SET created_at = ? WHERE id = ?;")
1481 .bind(base - index as i64)
1482 .bind(&order.id)
1483 .execute(&db.pool)
1484 .await
1485 .unwrap();
1486 ids.push(order.id);
1487 }
1488 ids
1491 }
1492
1493 fn window(limit: i64, offset: i64) -> OrderQuery {
1494 OrderQuery {
1495 limit,
1496 offset,
1497 ..OrderQuery::default()
1498 }
1499 }
1500
1501 #[tokio::test]
1502 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1503 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1504 let acct = account_id(&db).await;
1505 let ids = seed_orders(&db, "default", &acct, 5).await;
1506
1507 let (page, total) = Order::search(&window(2, 0), &db).await.unwrap();
1508 assert_eq!(total, 5, "the total must ignore the page window");
1509 assert_eq!(
1510 page.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
1511 ids[..2]
1512 );
1513
1514 let (second, total) = Order::search(&window(2, 2), &db).await.unwrap();
1515 assert_eq!(total, 5);
1516 assert_eq!(
1517 second.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
1518 ids[2..4]
1519 );
1520
1521 let (last, _) = Order::search(&window(2, 4), &db).await.unwrap();
1523 assert_eq!(last.len(), 1);
1524 let (beyond, total) = Order::search(&window(2, 99), &db).await.unwrap();
1525 assert!(beyond.is_empty());
1526 assert_eq!(total, 5, "a page past the end still reports the real total");
1527 }
1528
1529 #[tokio::test]
1532 async fn paging_one_row_at_a_time_sees_every_order_exactly_once() {
1533 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1534 let acct = account_id(&db).await;
1535 let mut expected = Vec::new();
1538 for index in 0..4 {
1539 let order = Order::create(
1540 "default",
1541 &acct,
1542 vec![Identifier::dns(format!("same-second-{index}.example.com"))],
1543 now_secs() + 3600,
1544 None,
1545 None,
1546 &db,
1547 )
1548 .await
1549 .unwrap();
1550 expected.push(order.id);
1551 }
1552 expected.sort();
1553
1554 let mut seen = Vec::new();
1555 for offset in 0..4 {
1556 let (page, total) = Order::search(&window(1, offset), &db).await.unwrap();
1557 assert_eq!(total, 4);
1558 assert_eq!(page.len(), 1);
1559 seen.push(page[0].id.clone());
1560 }
1561 seen.sort();
1562 assert_eq!(
1563 seen, expected,
1564 "pages must be disjoint and cover everything"
1565 );
1566 }
1567
1568 #[tokio::test]
1569 async fn search_filters_by_profile_account_and_status_together() {
1570 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1571 let acct = account_id(&db).await;
1572 let (other_account, _) = crate::sqlite::account::Account::find_or_create(
1573 "default",
1574 &[9u8, 9, 9],
1575 vec![],
1576 &ClientContext::default(),
1577 &db,
1578 )
1579 .await
1580 .unwrap();
1581
1582 seed_orders(&db, "default", &acct, 3).await;
1583 seed_orders(&db, "default", &other_account.id, 2).await;
1584 let mut ready = seed_orders(&db, "default", &acct, 1).await;
1585 let ready_id = ready.pop().unwrap();
1586 Order::find_by_id(&ready_id, &db)
1587 .await
1588 .unwrap()
1589 .unwrap()
1590 .mark_ready(&db)
1591 .await
1592 .unwrap();
1593
1594 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1596 assert_eq!(total, 6);
1597
1598 let by_account = OrderQuery {
1600 account_id: Some(acct.clone()),
1601 ..window(50, 0)
1602 };
1603 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1604 assert_eq!(total, 4);
1605 assert!(rows.iter().all(|o| o.account_id == acct));
1606
1607 let by_status = OrderQuery {
1609 status: Some(OrderStatus::Ready),
1610 ..window(50, 0)
1611 };
1612 let (rows, total) = Order::search(&by_status, &db).await.unwrap();
1613 assert_eq!(total, 1);
1614 assert_eq!(rows[0].id, ready_id);
1615
1616 let combined = OrderQuery {
1618 profile: Some("default".to_string()),
1619 account_id: Some(acct.clone()),
1620 status: Some(OrderStatus::Pending),
1621 limit: 50,
1622 offset: 0,
1623 };
1624 let (rows, total) = Order::search(&combined, &db).await.unwrap();
1625 assert_eq!(rows.len(), 3);
1626 assert_eq!(total, 3);
1627
1628 let none = OrderQuery {
1630 profile: Some("no-such-profile".to_string()),
1631 ..window(50, 0)
1632 };
1633 let (rows, total) = Order::search(&none, &db).await.unwrap();
1634 assert!(rows.is_empty());
1635 assert_eq!(total, 0);
1636 }
1637
1638 #[tokio::test]
1639 async fn search_scopes_by_profile() {
1640 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1641 let acct = account_id(&db).await;
1642 seed_orders(&db, "default", &acct, 2).await;
1643 seed_orders(&db, "other", &acct, 3).await;
1644
1645 let scoped = OrderQuery {
1646 profile: Some("other".to_string()),
1647 ..window(50, 0)
1648 };
1649 let (rows, total) = Order::search(&scoped, &db).await.unwrap();
1650 assert_eq!(total, 3);
1651 assert!(rows.iter().all(|o| o.profile == "other"));
1652 }
1653
1654 #[tokio::test]
1662 async fn a_filter_value_is_bound_not_interpolated() {
1663 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1664 let acct = account_id(&db).await;
1665 seed_orders(&db, "default", &acct, 2).await;
1666
1667 for hostile in ["' OR 1=1 --", "default'; DROP TABLE orders; --"] {
1668 let by_profile = OrderQuery {
1669 profile: Some(hostile.to_string()),
1670 ..window(50, 0)
1671 };
1672 let (rows, total) = Order::search(&by_profile, &db).await.unwrap();
1673 assert!(rows.is_empty(), "the value must be compared, not executed");
1674 assert_eq!(total, 0);
1675
1676 let by_account = OrderQuery {
1677 account_id: Some(hostile.to_string()),
1678 ..window(50, 0)
1679 };
1680 let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1681 assert!(rows.is_empty(), "the value must be compared, not executed");
1682 assert_eq!(total, 0);
1683 }
1684
1685 let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1687 assert_eq!(total, 2);
1688 }
1689}