Skip to main content

acme_proxy/sqlite/
order.rs

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/// An ACME identifier (RFC 8555 §7.1.4). Only `dns` is supported here, but the
15/// type is kept generic so the JSON round-trips whatever a client sent.
16///
17/// Stored inside the order's `identifiers` JSON array and echoed verbatim in the
18/// order object. Reused by the signer to check a finalize CSR's SANs.
19#[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    /// A `dns` identifier, which is every identifier this server issues for.
28    ///
29    /// Here rather than in a test helper because the struct had no constructor
30    /// at all, and twelve modules had each grown their own `fn dns(&str)` to
31    /// avoid writing the literal — the same accumulation that put `TempDir` in
32    /// `testutil`, except these are one line each and belong in production,
33    /// where the handlers building identifiers benefit too.
34    #[must_use]
35    pub fn dns(value: impl Into<String>) -> Self {
36        Self::new("dns", value)
37    }
38
39    /// An identifier of any type. `typ` is kept a free string because RFC 8555
40    /// §9.7.7 leaves the registry open and the order object echoes back
41    /// whatever a client sent.
42    #[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/// An ACME order (RFC 8555 §7.1.3). A new order is created in the `pending`
52/// state with one authorization per identifier; once every authorization is
53/// `valid` (its `http-01` challenge triggered) the order moves to `ready` and is
54/// finalizable.
55///
56/// ## Storage Details
57///
58/// - `identifiers` is persisted as a JSON array of `{type, value}` objects.
59/// - `error` is a nullable JSON problem document (set if issuance fails).
60/// - `certificate` holds the issued PEM chain, null until finalized.
61/// - `cert_serial`/`cert_pubkey` are populated alongside `certificate` (by
62///   [`Order::finalize`]) from the leaf's own serial (hex) and DER-SPKI public
63///   key — the former is how a `POST /revokeCert` request is looked up
64///   ([`Order::find_by_cert_serial`]), the latter is how it can be authorized
65///   by the certificate's own key pair (RFC 8555 §7.6's accountless case).
66/// - `revoked_at`/`revocation_reason` are this order's own revocation
67///   bookkeeping ([`Order::revoke`]), orthogonal to `status`: RFC 8555 defines
68///   no "revoked" order status, so a revoked order's `status` stays `valid`.
69/// - Timestamps are epoch seconds, matching accounts/nonces, and rendered as
70///   RFC3339 datetime strings in [`Order::to_json`].
71/// - The `authorizations` URLs are derived from the order's authorization ids
72///   (looked up separately and passed into [`Order::to_json`]); the `finalize`/
73///   `certificate` URLs are derived from the id + base URL, never stored (like
74///   `Account`'s `orders` URL).
75#[derive(Debug)]
76pub struct Order {
77    pub id: String,
78    /// The ACME endpoint (`[profiles.<name>]`) this order was placed at. It
79    /// always matches the owning account's own `profile` — the redundancy is
80    /// what lets the two lookups that take no account (`find_by_cert_serial`,
81    /// for revocation, and ARI) stay scoped to one endpoint.
82    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    /// The RFC 9773 §5 certID of the certificate this order is meant to
92    /// replace, when the client named one. Reflected back in [`Order::to_json`]
93    /// because §5 requires it: "If the server accepts a newOrder request with a
94    /// `replaces` field, it MUST reflect that field in the response and in
95    /// subsequent requests for the corresponding Order object."
96    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    /// Where `newOrder` was called from, and the reverse name that address had
103    /// at the time. Traceability only, never compared, and deliberately never
104    /// rendered by [`Order::to_json`] — see the schema comment in
105    /// `migrations/20260725120000_add_orders.sql`. There is no update-side
106    /// pair: the moment that matters after creation is issuance, which is an
107    /// `audit_log` row carrying its own address.
108    pub created_ip: Option<String>,
109    pub created_ptr: Option<String>,
110}
111
112/// The filters and page window [`Order::search`] applies.
113///
114/// Every field is optional except the window, and an absent filter imposes no
115/// constraint — so `OrderQuery { limit, offset, .. }` alone is "the newest
116/// page across every endpoint".
117#[derive(Debug, Clone, Default)]
118pub struct OrderQuery {
119    pub profile: Option<String>,
120    pub account_id: Option<String>,
121    pub status: Option<OrderStatus>,
122    /// Rows per page. The caller clamps this (`admin.page_size_max`); this
123    /// layer takes what it is given.
124    pub limit: i64,
125    pub offset: i64,
126}
127
128impl OrderQuery {
129    /// Appends the `WHERE` clause shared by the page query and the count.
130    ///
131    /// One function rather than two copies: a filter applied to only one of
132    /// them would report a total that does not match the rows returned, which
133    /// is the kind of bug a page control shows and nothing else does.
134    fn push_predicates(&self, builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>) {
135        let mut separator = " WHERE ";
136        // `status` arrives as an `OrderStatus` and the other two as `String`s,
137        // so each contributes its own `&str` and the array stays one type. The
138        // bind is still a parameter, never interpolated SQL.
139        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
155/// Renders epoch `secs` as an RFC3339 datetime string (the shape RFC 8555 uses
156/// for order datetime fields), falling back to an empty string for the
157/// out-of-range timestamps that should never occur in practice. Shared with the
158/// authorization/challenge model, which renders datetimes the same way.
159pub(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
166/// Every column, in one place: each lookup, both listings and the paged search
167/// must select the same set or [`Order::from_row`] fails on whichever forgot
168/// one.
169///
170/// A `macro_rules!` rather than a `const` so the expansion is a string
171/// *literal*: `sqlx::query` takes `impl SqlSafeStr`, which a runtime `format!`
172/// does not satisfy, so `concat!("SELECT ", columns!(), " FROM …")` is what
173/// keeps a shared column list and a compile-time-checked query in the same
174/// design.
175macro_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    /// Builds a new order in the `pending` state. Pure — nothing is persisted
220    /// until [`Order::insert`] runs.
221    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            // Set by `post_new_order` between here and `insert`, when the
241            // client sent one and it passed RFC 9773 §5's checks.
242            replaces: None,
243            cert_serial: None,
244            cert_pubkey: None,
245            revoked_at: None,
246            revocation_reason: None,
247            created_at: now_secs(),
248            // Filled in by `Order::with_client` between here and `insert`,
249            // exactly as `replaces` is and for the same reason: `new` already
250            // takes six positional arguments, and a seventh and eighth would
251            // put `Order::create` past the point where a reader can tell them
252            // apart without counting commas.
253            created_ip: None,
254            created_ptr: None,
255        }
256    }
257
258    /// Records where the order was placed from.
259    ///
260    /// Consuming rather than `&mut self` so it chains off [`Order::new`] at the
261    /// one call site that has a request behind it. An order created without it
262    /// — every test fixture, and any future path with no client — simply keeps
263    /// two `NULL`s, which is the honest answer.
264    #[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    /// Inserts the order using any executor — a pool, or a transaction.
272    ///
273    /// Split from [`Order::new`] so `post_new_order` can write the order and its
274    /// authorizations inside one transaction: a half-built order (fewer
275    /// authorizations than identifiers) would otherwise be finalizable for names
276    /// that were never authorized.
277    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        // `Identifier` derives `Serialize`, so this never fails in practice.
282        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    /// Creates a new order in the `pending` state (its authorizations are created
310    /// separately by the caller) and returns it.
311    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    /// Every order belonging to an account, newest first.
349    ///
350    /// Unfiltered on purpose: the admin CLI counts these to tell an operator
351    /// what a `DELETE` will cascade, and a filtered count would understate it.
352    /// The client-facing order-list URL wants
353    /// [`Order::find_active_by_account`] instead.
354    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    /// An account's orders that are still worth a client's attention, newest
372    /// first — what the RFC 8555 §7.1.2.1 order-list URL serves.
373    ///
374    /// §7.1.2.1: "The server SHOULD include pending orders and SHOULD NOT
375    /// include orders that are invalid in the array of URLs." Expired orders go
376    /// too: `load_owned_order` refuses one unless it is already `valid`, so
377    /// listing it would hand the client a URL that only ever answers with an
378    /// error.
379    ///
380    /// `valid` orders are kept whatever their `expires`, since the order
381    /// object's expiry is housekeeping (`order.validity_seconds`) and the
382    /// certificate it points at outlives it — that URL still works.
383    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    /// Lists orders across every account, oldest first — the admin CLI's
399    /// listing. `profile` filters to one endpoint (`None` lists all of them);
400    /// unlike [`Order::find_by_account`] (one account, newest-first, for the
401    /// order-list URL) there is no account filter — callers filter by
402    /// account/status client-side rather than building dynamic SQL for what is
403    /// expected to be a small, locally-run admin table.
404    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    /// One page of orders matching `query`, plus the total the same predicate
435    /// matches unpaged.
436    ///
437    /// Additive: [`Order::list_all`] is untouched, because the admin CLI counts
438    /// on getting everything. This exists because `orders` grows a row per
439    /// issuance forever — the first operator to open the web admin on a
440    /// year-old deployment would otherwise pull the whole table into memory and
441    /// JSON-encode it.
442    ///
443    /// The filters are in SQL rather than applied afterwards for the same
444    /// reason they have to be: filtering a page in memory would make the page
445    /// size wrong. It is also the *only* implementation of that policy — the
446    /// CLI's `order list` used to hold a second one in Rust over `list_all`,
447    /// which meant one meaning of `--status` written twice and a whole table
448    /// loaded to filter three fields.
449    ///
450    /// Built with a [`sqlx::QueryBuilder`]: `sqlx::query` takes only
451    /// `&'static str`, and every value below goes through `push_bind`, so
452    /// nothing operator- or client-supplied is ever interpolated into the SQL.
453    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        // Newest first, and `id` breaks the tie: `created_at` is whole seconds,
468        // so without it two orders placed in the same second could swap between
469        // pages and one of them would never be seen.
470        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    /// How many orders an account has.
493    ///
494    /// `COUNT(*)`, not `find_by_account(..).len()`: the only two callers want a
495    /// number, and loading every row means deserializing each one's
496    /// `identifiers` JSON to throw it away.
497    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    /// Hard-deletes the order row — cascading, via `ON DELETE CASCADE`, to its
509    /// authorizations and challenges. Returns whether a row existed to delete.
510    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    /// Records a successful issuance: stores the PEM `chain` plus the leaf's
527    /// `cert_serial` (hex) and `cert_pubkey` (DER SPKI) — populated by the
528    /// caller from that same chain via [`crate::cert::cert_serial_and_spki`],
529    /// since parsing needs error handling the DB layer doesn't otherwise deal
530    /// in — moves the order to the terminal `valid` state, and keeps `self`
531    /// in sync so a following [`Order::to_json`] reflects the change without
532    /// a re-read.
533    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    /// Looks up the order whose stored certificate carries `serial` (hex,
560    /// matching `cert_serial`'s format) — indexed, so a `POST /revokeCert`
561    /// request is not a full table scan across every issued order. **Not**
562    /// proof of identity on its own: the caller must additionally compare the
563    /// submitted certificate's DER against the returned order's stored chain
564    /// byte-for-byte (a random-serial collision, or a crafted certificate
565    /// reusing a real serial, are not ruled out by the serial alone).
566    ///
567    /// Scoped to `profile`: revocation and ARI carry no account, so the
568    /// endpoint the request arrived at is the only thing that keeps one
569    /// profile from answering for — or revoking — another's certificate.
570    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    /// Finds any order that already claims to replace `cert_id` and is not
596    /// `invalid` — RFC 9773 §5's "the identified certificate has not already
597    /// been marked as replaced by a different Order that is not `invalid`".
598    ///
599    /// The `invalid` exclusion is what makes a retry work: an order that failed
600    /// validation never produced a replacement, so it must not hold the
601    /// predecessor hostage. Scoped by profile like every other request-path
602    /// lookup — a certID is only meaningful at the endpoint that issued it.
603    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    /// Records a certificate revocation (RFC 8555 §7.6): stamps `revoked_at`
623    /// (now) and the optional `CRLReason` `reason`, and keeps `self` in sync
624    /// (like [`Order::finalize`]). Deliberately does **not** touch `status`:
625    /// revocation is orthogonal to the order state machine (RFC 8555 defines
626    /// no "revoked" order status), so a revoked order stays `valid`.
627    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    /// Records a failed issuance: stores the `error` problem document, moves the
648    /// order to the terminal `invalid` state, and keeps `self` in sync (like
649    /// [`Order::finalize`]). Used when the signer fails internally; a `badCSR`
650    /// leaves the order `ready` and retryable instead.
651    /// The `invalid` transition as a bare statement, over any executor.
652    ///
653    /// Split from [`Order::mark_invalid`] so `post_challenge` can compose the
654    /// challenge, authorization and order transitions into one transaction. The
655    /// in-memory sync stays in `mark_invalid`, since it must not happen until
656    /// the transaction has committed.
657    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        // `error` is a `serde_json::Value`, so serialization is infallible.
666        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    /// The `ready` transition as a bare statement; see [`Order::set_invalid`].
675    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    /// The `pending` transition as a bare statement; see [`Order::set_invalid`].
687    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    /// Moves the order from `pending` to `ready` once all its authorizations are
713    /// `valid`, so it can be finalized. Keeps `self` in sync (like
714    /// [`Order::finalize`]).
715    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    /// Moves the order back from `ready` to `pending`, after one of its
725    /// authorizations stopped being `valid` — in practice, a client
726    /// deactivating one (RFC 8555 §7.5.2).
727    ///
728    /// The only backwards transition in the order state machine, and it exists
729    /// because §7.5.2's "the server MUST NOT treat deactivated authorization
730    /// objects as sufficient for issuing certificates" has to hold for an order
731    /// that already reached `ready` — otherwise `finalize` would still accept
732    /// it. RFC 8555 §7.1.6's diagram draws `pending → ready` as the state
733    /// becoming true rather than a one-way latch, so re-deriving it is in
734    /// keeping with the model.
735    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    /// Claims the order for issuance, moving it from `ready` to `processing` in
745    /// **one guarded statement**: `Ok(true)` means this caller won the claim,
746    /// `Ok(false)` that somebody else already holds it. Keeps `self` in sync
747    /// (like [`Order::mark_ready`]) only on the winning branch.
748    ///
749    /// **The precondition is the whole point.** `post_finalize` reads the order,
750    /// checks it is `ready`, signs, and writes — three steps with no lock
751    /// between them, so N concurrent finalize requests on one order all passed
752    /// the check, all reached `SignerBackend::issue`, and all got a certificate
753    /// back. Only the last write survived, and the others became valid
754    /// CA-signed certificates with no row naming their serial: `POST
755    /// /revokeCert` looks an order up by `find_by_cert_serial` and would answer
756    /// "unknown certificate", so nothing this server offers could ever revoke
757    /// them and the CRL would never learn they exist. `rows_affected` closes it,
758    /// the primitive [`crate::sqlite::nonce::Nonce::verify`] and
759    /// `AdminUser::claim_totp_step` already rest on.
760    ///
761    /// The `relay` backend was never exposed, because `upstream_orders.order_id`
762    /// is a primary key and the second insert conflicts — this gives `local_ca`
763    /// and `custom`, which answer inline, the same guard.
764    ///
765    /// The `processing` status needed no migration — the `orders.status`
766    /// `CHECK` has always allowed it; until the `relay` backend existed
767    /// there was simply no asynchronous issuance to use it.
768    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    /// Gives the claim back, moving `processing` to `ready` so the client can
790    /// try again — the counterpart to [`Order::claim_for_finalize`] for the
791    /// refusals RFC 8555 §7.4 says must leave the order finalizable (a rejected
792    /// CSR) and for the two arms where issuance succeeded but this server could
793    /// not read what it had just been handed.
794    ///
795    /// Guarded on `processing` for a reason of its own: a §7.5.2 deactivation
796    /// racing this claim demotes the order to `pending`
797    /// ([`Order::mark_pending`], unguarded, since it is the authoritative
798    /// answer to an authorization that stopped being valid). An unguarded
799    /// release would push it back to `ready` and hand the client a finalizable
800    /// order whose authorizations no longer support it.
801    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    /// The RFC 8555 order object. URLs are derived from `base_url`; datetimes are
819    /// rendered RFC3339. `authorizations` lists one URL per `authz_ids` entry, the
820    /// `certificate` URL appears only once the order is `valid`, and `notBefore`/
821    /// `notAfter`/`error` appear only when set.
822    #[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        // RFC 9773 §5: the field is reflected "in the response and in
859        // subsequent requests for the corresponding Order object" — so it is
860        // rendered here, not just echoed once on the 201.
861        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    /// `with_client` is set between `new` and `insert`, the way `replaces` is,
874    /// and — like `replaces` — it has to survive the round trip. An order built
875    /// without it keeps two `NULL`s rather than empty strings.
876    #[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        // And the ACME order object says nothing about either: RFC 8555 §7.1.3
914        // defines its members, and where a client connected from is not one.
915        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        // A pending order has no certificate URL yet, and no notBefore/notAfter.
1026        assert!(json.get("certificate").is_none());
1027        assert!(json.get("notBefore").is_none());
1028        assert!(json.get("notAfter").is_none());
1029        // `expires` renders as an RFC3339 string ending in Z.
1030        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        // In-memory struct is updated…
1083        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        // …and so is the stored row, and to_json now exposes the certificate URL.
1088        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    /// The guard the whole double-issuance fix rests on: two callers race, and
1100    /// exactly one of them may go on to ask a signer for a certificate.
1101    #[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        // A second handle on the same row, as a concurrent request would have.
1120        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        // The loser is told so, and its own in-memory copy is left alone rather
1126        // than being synced to a status it does not hold the claim on.
1127        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    /// Every status but `ready` refuses the claim — `pending` because the
1135    /// authorizations do not support issuance yet, `valid` because a
1136    /// certificate already exists, `invalid` because the order is terminal.
1137    #[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            // `pending` is the state `create` leaves behind.
1144            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    /// The release is the counterpart RFC 8555 §7.4 needs for a rejected CSR,
1181    /// and it is guarded so a §7.5.2 deactivation racing it wins.
1182    #[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        // Now the race: the claim is held, a deactivation demotes the order,
1207        // and the release must not hand the client back a finalizable order
1208        // whose authorizations no longer support it.
1209        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        // In-memory struct is updated, and `status` is untouched…
1269        assert!(order.revoked_at.is_some());
1270        assert_eq!(order.revocation_reason, Some(1));
1271        assert_eq!(order.status, OrderStatus::Valid);
1272        // …and so is the stored row.
1273        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        // In-memory struct is updated…
1329        assert_eq!(order.status, OrderStatus::Invalid);
1330        assert_eq!(order.error, Some(error.clone()));
1331        // …and so is the stored row, and to_json now exposes the error object.
1332        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    /// Seeds `count` orders under `profile`, each backdated one second further
1458    /// than the last so `created_at DESC` has a deterministic answer without
1459    /// relying on the UUID tiebreak.
1460    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        // Newest first is the order `search` returns, and `ids` is already in
1489        // that order: index 0 was backdated least.
1490        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        // A partial last page, then past the end.
1522        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    /// Every page must be disjoint and together cover everything: the property
1530    /// the `created_at, id` tiebreak exists for.
1531    #[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        // Deliberately NOT backdated: all four share one `created_at`, which is
1536        // exactly the case where a missing tiebreak lets rows swap pages.
1537        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        // No filter at all: everything.
1595        let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1596        assert_eq!(total, 6);
1597
1598        // By account.
1599        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        // By status.
1608        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        // All three at once, and the count must agree with the rows.
1617        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        // A filter matching nothing is empty, not an error.
1629        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    /// A value that would be SQL if it were interpolated instead of bound.
1655    ///
1656    /// `status` used to be the vector here, and is no longer expressible: it is
1657    /// an [`OrderStatus`], so a hostile value cannot reach this layer at all —
1658    /// `Order::search` never sees one, because `--status` and `?status=` refuse
1659    /// it by name first. `profile` and `account_id` are still free strings and
1660    /// still go through `push_bind`, so the property is asserted on those.
1661    #[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        // The table is still there, which is what the second vector is for.
1686        let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1687        assert_eq!(total, 2);
1688    }
1689}