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/// - `cert_not_after` is populated the same way and from the same leaf, and is
67///   **not** `not_after`: that one is the validity the client *asked* for
68///   (§7.4), usually absent and clamped by the signer when it is not, where
69///   this is what the certificate says. `NULL` means the row predates the
70///   column and nothing has parsed its chain yet; a negative value means the
71///   sweep parsed it and could not (see the migration).
72/// - `revoked_at`/`revocation_reason` are this order's own revocation
73///   bookkeeping ([`Order::revoke`]), orthogonal to `status`: RFC 8555 defines
74///   no "revoked" order status, so a revoked order's `status` stays `valid`.
75/// - Timestamps are epoch seconds, matching accounts/nonces, and rendered as
76///   RFC3339 datetime strings in [`Order::to_json`].
77/// - The `authorizations` URLs are derived from the order's authorization ids
78///   (looked up separately and passed into [`Order::to_json`]); the `finalize`/
79///   `certificate` URLs are derived from the id + base URL, never stored (like
80///   `Account`'s `orders` URL).
81#[derive(Debug)]
82pub struct Order {
83    pub id: String,
84    /// The ACME endpoint (`[profiles.<name>]`) this order was placed at. It
85    /// always matches the owning account's own `profile` — the redundancy is
86    /// what lets the two lookups that take no account (`find_by_cert_serial`,
87    /// for revocation, and ARI) stay scoped to one endpoint.
88    pub profile: String,
89    pub account_id: String,
90    pub status: OrderStatus,
91    pub identifiers: Vec<Identifier>,
92    pub expires: i64,
93    pub not_before: Option<i64>,
94    pub not_after: Option<i64>,
95    pub error: Option<Value>,
96    pub certificate: Option<String>,
97    /// The RFC 9773 §5 certID of the certificate this order is meant to
98    /// replace, when the client named one. Reflected back in [`Order::to_json`]
99    /// because §5 requires it: "If the server accepts a newOrder request with a
100    /// `replaces` field, it MUST reflect that field in the response and in
101    /// subsequent requests for the corresponding Order object."
102    pub replaces: Option<String>,
103    pub cert_serial: Option<String>,
104    pub cert_pubkey: Option<Vec<u8>>,
105    /// The leaf's own notAfter, epoch seconds. `None` on a row finalized before
106    /// the column existed (the digest's backfill stamps those), and
107    /// [`UNPARSABLE_NOT_AFTER`] once the backfill has looked and failed.
108    pub cert_not_after: Option<i64>,
109    pub revoked_at: Option<i64>,
110    pub revocation_reason: Option<i64>,
111    pub created_at: i64,
112    /// Where `newOrder` was called from, and the reverse name that address had
113    /// at the time. Traceability only, never compared, and deliberately never
114    /// rendered by [`Order::to_json`] — see the schema comment in
115    /// `migrations/20260725120000_add_orders.sql`. There is no update-side
116    /// pair: the moment that matters after creation is issuance, which is an
117    /// `audit_log` row carrying its own address.
118    pub created_ip: Option<String>,
119    pub created_ptr: Option<String>,
120}
121
122/// The filters and page window [`Order::search`] applies.
123///
124/// Every field is optional except the window, and an absent filter imposes no
125/// constraint — so `OrderQuery { limit, offset, .. }` alone is "the newest
126/// page across every endpoint".
127#[derive(Debug, Clone, Default)]
128pub struct OrderQuery {
129    pub profile: Option<String>,
130    pub account_id: Option<String>,
131    pub status: Option<OrderStatus>,
132    /// Rows per page. The caller clamps this (`admin.page_size_max`); this
133    /// layer takes what it is given.
134    pub limit: i64,
135    pub offset: i64,
136}
137
138impl OrderQuery {
139    /// Appends the `WHERE` clause shared by the page query and the count.
140    ///
141    /// One function rather than two copies: a filter applied to only one of
142    /// them would report a total that does not match the rows returned, which
143    /// is the kind of bug a page control shows and nothing else does.
144    fn push_predicates(&self, builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>) {
145        let mut separator = " WHERE ";
146        // `status` arrives as an `OrderStatus` and the other two as `String`s,
147        // so each contributes its own `&str` and the array stays one type. The
148        // bind is still a parameter, never interpolated SQL.
149        for (column, value) in [
150            ("profile = ", self.profile.as_deref()),
151            ("account_id = ", self.account_id.as_deref()),
152            ("status = ", self.status.map(OrderStatus::as_str)),
153        ] {
154            if let Some(value) = value {
155                builder
156                    .push(separator)
157                    .push(column)
158                    .push_bind(value.to_string());
159                separator = " AND ";
160            }
161        }
162    }
163}
164
165/// Renders epoch `secs` as an RFC3339 datetime string (the shape RFC 8555 uses
166/// for order datetime fields), falling back to an empty string for the
167/// out-of-range timestamps that should never occur in practice. Shared with the
168/// authorization/challenge model, which renders datetimes the same way.
169pub(crate) fn rfc3339(secs: i64) -> String {
170    OffsetDateTime::from_unix_timestamp(secs)
171        .ok()
172        .and_then(|dt| dt.format(&Rfc3339).ok())
173        .unwrap_or_default()
174}
175
176/// Every column, in one place: each lookup, both listings and the paged search
177/// must select the same set or [`Order::from_row`] fails on whichever forgot
178/// one.
179///
180/// A `macro_rules!` rather than a `const` so the expansion is a string
181/// *literal*: `sqlx::query` takes `impl SqlSafeStr`, which a runtime `format!`
182/// does not satisfy, so `concat!("SELECT ", columns!(), " FROM …")` is what
183/// keeps a shared column list and a compile-time-checked query in the same
184/// design.
185macro_rules! columns {
186    () => {
187        "id, profile, account_id, status, identifiers, expires, not_before, not_after, \
188         error, certificate, replaces, cert_serial, cert_pubkey, cert_not_after, \
189         revoked_at, revocation_reason, created_at, created_ip, created_ptr"
190    };
191}
192
193/// What [`Order::cert_not_after`] holds for a chain that would not parse, so it
194/// is never parsed again.
195///
196/// Any negative value would do, and every reader tests the *sign* rather than
197/// comparing against this — the column is documented as "negative means
198/// unparsable". It is named here, beside the column, because three modules now
199/// write or skip it: the digest's backfill, the expiry predicates below, and
200/// the supersession annotation in `crate::admin`.
201pub const UNPARSABLE_NOT_AFTER: i64 = -1;
202
203/// The expiry listing's `WHERE`, appended to both [`Order::find_expiring`]'s
204/// page and its count for the reason [`OrderQuery::push_predicates`] is one
205/// function: a predicate applied to only one of them reports a total the rows
206/// do not match, which a page control shows and nothing else does.
207///
208/// A function over a `QueryBuilder` rather than the `macro_rules!` this used to
209/// be, because `profile` became optional when the admin surfaces arrived and a
210/// `concat!` literal cannot carry a conditional clause. The three predicates
211/// after it are unconditional and each carries its reason on
212/// [`Order::find_expiring`].
213fn push_expiring_predicates(
214    profile: Option<&str>,
215    before: i64,
216    builder: &mut sqlx::QueryBuilder<sqlx::Sqlite>,
217) {
218    builder.push(" FROM orders WHERE certificate IS NOT NULL AND revoked_at IS NULL");
219    builder.push(" AND cert_not_after >= 0 AND cert_not_after <= ");
220    builder.push_bind(before);
221    if let Some(profile) = profile {
222        builder.push(" AND profile = ");
223        builder.push_bind(profile.to_string());
224    }
225}
226
227impl Order {
228    fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
229        let identifiers_json: String = row.try_get("identifiers")?;
230        let identifiers: Vec<Identifier> = serde_json::from_str(&identifiers_json)
231            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
232
233        let error_json: Option<String> = row.try_get("error")?;
234        let error: Option<Value> = match error_json {
235            Some(text) => {
236                Some(serde_json::from_str(&text).map_err(|e| sqlx::Error::Decode(Box::new(e)))?)
237            }
238            None => None,
239        };
240
241        Ok(Order {
242            id: row.try_get("id")?,
243            profile: row.try_get("profile")?,
244            account_id: row.try_get("account_id")?,
245            status: status::from_column(row.try_get::<&str, _>("status")?)?,
246            identifiers,
247            expires: row.try_get("expires")?,
248            not_before: row.try_get("not_before")?,
249            not_after: row.try_get("not_after")?,
250            error,
251            certificate: row.try_get("certificate")?,
252            replaces: row.try_get("replaces")?,
253            cert_serial: row.try_get("cert_serial")?,
254            cert_pubkey: row.try_get("cert_pubkey")?,
255            cert_not_after: row.try_get("cert_not_after")?,
256            revoked_at: row.try_get("revoked_at")?,
257            revocation_reason: row.try_get("revocation_reason")?,
258            created_at: row.try_get("created_at")?,
259            created_ip: row.try_get("created_ip")?,
260            created_ptr: row.try_get("created_ptr")?,
261        })
262    }
263
264    /// Builds a new order in the `pending` state. Pure — nothing is persisted
265    /// until [`Order::insert`] runs.
266    pub(crate) fn new(
267        profile: &str,
268        account_id: &str,
269        identifiers: Vec<Identifier>,
270        expires: i64,
271        not_before: Option<i64>,
272        not_after: Option<i64>,
273    ) -> Order {
274        Order {
275            id: Uuid::new_v4().to_string(),
276            profile: profile.to_string(),
277            account_id: account_id.to_string(),
278            status: OrderStatus::Pending,
279            identifiers,
280            expires,
281            not_before,
282            not_after,
283            error: None,
284            certificate: None,
285            // Set by `post_new_order` between here and `insert`, when the
286            // client sent one and it passed RFC 9773 §5's checks.
287            replaces: None,
288            cert_serial: None,
289            cert_pubkey: None,
290            cert_not_after: None,
291            revoked_at: None,
292            revocation_reason: None,
293            created_at: now_secs(),
294            // Filled in by `Order::with_client` between here and `insert`,
295            // exactly as `replaces` is and for the same reason: `new` already
296            // takes six positional arguments, and a seventh and eighth would
297            // put `Order::create` past the point where a reader can tell them
298            // apart without counting commas.
299            created_ip: None,
300            created_ptr: None,
301        }
302    }
303
304    /// Records where the order was placed from.
305    ///
306    /// Consuming rather than `&mut self` so it chains off [`Order::new`] at the
307    /// one call site that has a request behind it. An order created without it
308    /// — every test fixture, and any future path with no client — simply keeps
309    /// two `NULL`s, which is the honest answer.
310    #[must_use]
311    pub(crate) fn with_client(mut self, client: &crate::audit::ClientContext) -> Order {
312        self.created_ip = client.ip.clone();
313        self.created_ptr = client.ptr.clone();
314        self
315    }
316
317    /// Inserts the order using any executor — a pool, or a transaction.
318    ///
319    /// Split from [`Order::new`] so `post_new_order` can write the order and its
320    /// authorizations inside one transaction: a half-built order (fewer
321    /// authorizations than identifiers) would otherwise be finalizable for names
322    /// that were never authorized.
323    pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
324    where
325        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
326    {
327        // `Identifier` derives `Serialize`, so this never fails in practice.
328        let identifiers_json = serde_json::to_string(&self.identifiers)
329            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
330
331        debug!(event = "db_order_create_started", outcome = "progress", order_id = ?self.id, profile = %self.profile, account_id = ?self.account_id);
332        sqlx::query(
333            "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, not_before, not_after, error, certificate, replaces, created_at, created_ip, created_ptr) \
334             VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?);",
335        )
336        .bind(&self.id)
337        .bind(&self.profile)
338        .bind(&self.account_id)
339        .bind(self.status.as_str())
340        .bind(identifiers_json)
341        .bind(self.expires)
342        .bind(self.not_before)
343        .bind(self.not_after)
344        .bind(&self.replaces)
345        .bind(self.created_at)
346        .bind(&self.created_ip)
347        .bind(&self.created_ptr)
348        .execute(executor)
349        .await?;
350
351        debug!(event = "db_order_created", outcome = "success", order_id = ?self.id, account_id = ?self.account_id);
352        Ok(())
353    }
354
355    /// Creates a new order in the `pending` state (its authorizations are created
356    /// separately by the caller) and returns it.
357    pub async fn create(
358        profile: &str,
359        account_id: &str,
360        identifiers: Vec<Identifier>,
361        expires: i64,
362        not_before: Option<i64>,
363        not_after: Option<i64>,
364        database: &Database,
365    ) -> Result<Order, sqlx::Error> {
366        let order = Order::new(
367            profile,
368            account_id,
369            identifiers,
370            expires,
371            not_before,
372            not_after,
373        );
374        order.insert(&database.pool).await?;
375        Ok(order)
376    }
377
378    pub async fn find_by_id(id: &str, database: &Database) -> Result<Option<Order>, sqlx::Error> {
379        debug!(event = "db_order_find_by_id_started", outcome = "progress", order_id = ?id);
380        let row = sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE id = ?;"))
381            .bind(id)
382            .fetch_optional(&database.pool)
383            .await?;
384
385        let result = row.map(Order::from_row).transpose()?;
386        if result.is_some() {
387            info!(event = "db_order_found_by_id", outcome = "success", order_id = ?id);
388        } else {
389            debug!(event = "db_order_not_found_by_id", outcome = "failure", order_id = ?id);
390        }
391        Ok(result)
392    }
393
394    /// Every order belonging to an account, newest first.
395    ///
396    /// Unfiltered on purpose: the admin CLI counts these to tell an operator
397    /// what a `DELETE` will cascade, and a filtered count would understate it.
398    /// The client-facing order-list URL wants
399    /// [`Order::find_active_by_account`] instead.
400    pub async fn find_by_account(
401        account_id: &str,
402        database: &Database,
403    ) -> Result<Vec<Order>, sqlx::Error> {
404        debug!(event = "db_order_find_by_account_started", outcome = "progress", account_id = ?account_id);
405        let rows = sqlx::query(concat!(
406            "SELECT ",
407            columns!(),
408            " FROM orders WHERE account_id = ? ORDER BY created_at DESC;"
409        ))
410        .bind(account_id)
411        .fetch_all(&database.pool)
412        .await?;
413
414        rows.into_iter().map(Order::from_row).collect()
415    }
416
417    /// An account's orders that are still worth a client's attention, newest
418    /// first — what the RFC 8555 §7.1.2.1 order-list URL serves.
419    ///
420    /// §7.1.2.1: "The server SHOULD include pending orders and SHOULD NOT
421    /// include orders that are invalid in the array of URLs." Expired orders go
422    /// too: `load_owned_order` refuses one unless it is already `valid`, so
423    /// listing it would hand the client a URL that only ever answers with an
424    /// error.
425    ///
426    /// `valid` orders are kept whatever their `expires`, since the order
427    /// object's expiry is housekeeping (`order.validity_seconds`) and the
428    /// certificate it points at outlives it — that URL still works.
429    pub async fn find_active_by_account(
430        account_id: &str,
431        database: &Database,
432    ) -> Result<Vec<Order>, sqlx::Error> {
433        debug!(event = "db_order_find_active_by_account_started", outcome = "progress", account_id = ?account_id);
434        let rows =
435            sqlx::query(concat!("SELECT ", columns!(), " FROM orders WHERE account_id = ? AND status != 'invalid' AND (status = 'valid' OR expires > ?) ORDER BY created_at DESC;"))
436                .bind(account_id)
437                .bind(now_secs())
438                .fetch_all(&database.pool)
439                .await?;
440
441        rows.into_iter().map(Order::from_row).collect()
442    }
443
444    /// One page of orders matching `query`, plus the total the same predicate
445    /// matches unpaged.
446    ///
447    /// The **only** cross-account listing this model offers. An unpaged
448    /// `list_all` stood beside it, oldest first, until both front ends took a
449    /// window; `orders` grows a row per issuance forever, so an operator
450    /// opening either surface on a year-old deployment would otherwise pull the
451    /// whole table into memory and render it.
452    ///
453    /// The filters are in SQL rather than applied afterwards for the same
454    /// reason they have to be: filtering a page in memory would make the page
455    /// size wrong. It is also the *only* implementation of that policy — the
456    /// CLI's `order list` used to hold a second one in Rust over the unpaged
457    /// listing, which meant one meaning of `--status` written twice and a whole
458    /// table loaded to filter three fields.
459    ///
460    /// Built with a [`sqlx::QueryBuilder`]: `sqlx::query` takes only
461    /// `&'static str`, and every value below goes through `push_bind`, so
462    /// nothing operator- or client-supplied is ever interpolated into the SQL.
463    pub async fn search(
464        query: &OrderQuery,
465        database: &Database,
466    ) -> Result<(Vec<Order>, i64), sqlx::Error> {
467        debug!(event = "db_order_search_started",
468               outcome = "progress",
469               profile = ?query.profile,
470               account_id = ?query.account_id,
471               status = ?query.status,
472               limit = query.limit,
473               offset = query.offset);
474
475        let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!(), " FROM orders"));
476        query.push_predicates(&mut page);
477        // Newest first, and `id` breaks the tie: `created_at` is whole seconds,
478        // so without it two orders placed in the same second could swap between
479        // pages and one of them would never be seen.
480        page.push(" ORDER BY created_at DESC, id DESC LIMIT ");
481        page.push_bind(query.limit);
482        page.push(" OFFSET ");
483        page.push_bind(query.offset);
484
485        let rows = page.build().fetch_all(&database.pool).await?;
486        let orders: Vec<Order> = rows
487            .into_iter()
488            .map(Order::from_row)
489            .collect::<Result<_, _>>()?;
490
491        let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*) FROM orders");
492        query.push_predicates(&mut count);
493        let total: i64 = count
494            .build()
495            .fetch_one(&database.pool)
496            .await?
497            .try_get::<i64, _>(0)?;
498
499        Ok((orders, total))
500    }
501
502    /// Deletes this profile's orders that expired before `cutoff`, returning
503    /// how many went. The `authorizations` and `challenges` beneath them go
504    /// with the row, through the schema's `ON DELETE CASCADE`.
505    ///
506    /// **`valid` is excluded, whatever the age.** A valid order's row is how
507    /// `Order::find_by_cert_serial` resolves a certificate for `revokeCert` and
508    /// for the CRL, and what RFC 9773 renewal information is derived from —
509    /// deleting one would make an issued certificate unrevokable and
510    /// unrenewable, which is a far worse outcome than a large table. Everything
511    /// else is an order no client can act on any more: `invalid` is terminal,
512    /// and a `pending`/`ready`/`processing` order past its own `expires` is
513    /// refused on read by every handler that loads one.
514    ///
515    /// Scoped to one profile because `order.retention_days` is a per-profile
516    /// key: two endpoints in one process may reasonably keep their history for
517    /// different lengths of time.
518    pub async fn cleanup(
519        profile: &str,
520        cutoff: i64,
521        database: &Database,
522    ) -> Result<u64, sqlx::Error> {
523        debug!(event = "db_order_cleanup_started", outcome = "progress", profile = %profile, cutoff = cutoff);
524        let removed = sqlx::query(
525            "DELETE FROM orders WHERE profile = ? AND status != 'valid' AND expires < ?;",
526        )
527        .bind(profile)
528        .bind(cutoff)
529        .execute(&database.pool)
530        .await?
531        .rows_affected();
532
533        debug!(event = "db_order_cleanup_completed", outcome = "success", profile = %profile, rows_removed = removed);
534        Ok(removed)
535    }
536
537    /// How many orders an account has.
538    ///
539    /// `COUNT(*)`, not `find_by_account(..).len()`: the only two callers want a
540    /// number, and loading every row means deserializing each one's
541    /// `identifiers` JSON to throw it away.
542    pub async fn count_by_account(
543        account_id: &str,
544        database: &Database,
545    ) -> Result<i64, sqlx::Error> {
546        let row = sqlx::query("SELECT COUNT(*) FROM orders WHERE account_id = ?;")
547            .bind(account_id)
548            .fetch_one(&database.pool)
549            .await?;
550        row.try_get::<i64, _>(0)
551    }
552
553    /// Hard-deletes the order row — cascading, via `ON DELETE CASCADE`, to its
554    /// authorizations and challenges. Returns whether a row existed to delete.
555    pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
556        debug!(event = "db_order_delete_started", outcome = "progress", order_id = ?id);
557        let result = sqlx::query("DELETE FROM orders WHERE id = ?;")
558            .bind(id)
559            .execute(&database.pool)
560            .await?;
561
562        let deleted = result.rows_affected() > 0;
563        if deleted {
564            info!(event = "db_order_deleted", outcome = "success", order_id = ?id);
565        } else {
566            debug!(event = "db_order_delete_missing", outcome = "success", order_id = ?id);
567        }
568        Ok(deleted)
569    }
570
571    /// Records a successful issuance: stores the PEM `chain` plus the leaf's
572    /// `cert_serial` (hex), `cert_pubkey` (DER SPKI) and `cert_not_after` —
573    /// all three populated by the caller from that same chain via
574    /// [`crate::cert::cert_serial_and_spki`] and [`crate::cert::cert_validity`],
575    /// since parsing needs error handling the DB layer doesn't otherwise deal
576    /// in — moves the order to the terminal `valid` state, and keeps `self`
577    /// in sync so a following [`Order::to_json`] reflects the change without
578    /// a re-read.
579    ///
580    /// `cert_not_after` is `Option` where the other two are not, and the
581    /// asymmetry is deliberate: the serial and the public key are what make a
582    /// certificate revocable, so a chain they cannot be read from is a failed
583    /// issuance, while the expiry is housekeeping for the digest and a leaf
584    /// this server cannot read the validity of must still be recorded as
585    /// issued. Callers pass `None` rather than refusing — the rule
586    /// `LocalCa::revoke` already follows when it records a revoked leaf's
587    /// expiry, and the sweep will try the chain again later.
588    pub async fn finalize(
589        &mut self,
590        chain: String,
591        cert_serial: String,
592        cert_pubkey: Vec<u8>,
593        cert_not_after: Option<i64>,
594        database: &Database,
595    ) -> Result<(), sqlx::Error> {
596        debug!(event = "db_order_finalize_started", outcome = "progress", order_id = ?self.id);
597        sqlx::query(
598            "UPDATE orders SET certificate = ?, cert_serial = ?, cert_pubkey = ?, \
599             cert_not_after = ?, status = 'valid' WHERE id = ?;",
600        )
601        .bind(&chain)
602        .bind(&cert_serial)
603        .bind(&cert_pubkey)
604        .bind(cert_not_after)
605        .bind(&self.id)
606        .execute(&database.pool)
607        .await?;
608
609        self.certificate = Some(chain);
610        self.cert_serial = Some(cert_serial);
611        self.cert_pubkey = Some(cert_pubkey);
612        self.cert_not_after = cert_not_after;
613        self.status = OrderStatus::Valid;
614        debug!(event = "db_order_finalized", outcome = "success", order_id = ?self.id);
615        Ok(())
616    }
617
618    /// The certificates expiring at or before `before`, soonest first, with the
619    /// unpaged total beside the page — the digest's whole query
620    /// (`[notify.expiry]`, [`crate::notify::expiry`]) and the admin surfaces'
621    /// (`GET /api/expiring`, `/ui/expiring`, `order list --expiring-in`).
622    ///
623    /// Three predicates, each carrying its own reason. `certificate IS NOT
624    /// NULL` because an order that never issued has nothing to expire;
625    /// `revoked_at IS NULL` because a withdrawn certificate is not something to
626    /// go and renew; and `cert_not_after >= 0` because a negative value is the
627    /// sweep's sentinel for a chain it could not parse, which is a row to leave
628    /// alone rather than to report as expiring in 1970. They are exactly the
629    /// partial index's own predicate.
630    ///
631    /// `profile` is an `Option` because the panel lists every endpoint by
632    /// default, like every other admin listing, where the digest asks one
633    /// profile at a time. The two forms cost different things, and the
634    /// difference is the index's column order:
635    ///
636    /// - `Some` is `SEARCH … USING INDEX idx_orders_cert_not_after (profile=?
637    ///   AND cert_not_after>? AND cert_not_after<?)` — a range seek on both
638    ///   columns, and byte for byte the plan the digest's original query got.
639    /// - `None` is `SCAN … USING INDEX idx_orders_cert_not_after`: the partial
640    ///   predicate still matches, so the index is still what is read, but with
641    ///   no leading-column equality there is nothing to seek to, and the index
642    ///   is ordered by `cert_not_after` only *within* a profile — so the
643    ///   ordering below falls to a temp b-tree over the whole result rather
644    ///   than over its last term alone.
645    ///
646    /// That is the price of the unscoped view, and it is stated here rather
647    /// than left to be rediscovered from a query plan. A profile-less index on
648    /// `cert_not_after` would buy it back, and is not worth a second index on
649    /// a table this one already covers until a deployment says otherwise.
650    ///
651    /// The total is counted rather than derived from the page, so "…and N more"
652    /// can be honest without loading a tail nobody will read. `id` breaks the
653    /// ordering tie for [`Order::search`]'s reason: two certificates can share
654    /// a whole-second expiry, and a stable order is what stops one of them
655    /// being dropped between the page and the count.
656    pub async fn find_expiring(
657        profile: Option<&str>,
658        before: i64,
659        limit: i64,
660        offset: i64,
661        database: &Database,
662    ) -> Result<(Vec<Order>, i64), sqlx::Error> {
663        debug!(
664            event = "db_order_find_expiring_started",
665            outcome = "progress",
666            profile = ?profile,
667            before,
668            limit,
669            offset
670        );
671        let mut page = sqlx::QueryBuilder::new(concat!("SELECT ", columns!()));
672        push_expiring_predicates(profile, before, &mut page);
673        page.push(" ORDER BY cert_not_after ASC, id ASC LIMIT ");
674        page.push_bind(limit);
675        page.push(" OFFSET ");
676        page.push_bind(offset);
677
678        let rows = page.build().fetch_all(&database.pool).await?;
679        let orders: Vec<Order> = rows
680            .into_iter()
681            .map(Order::from_row)
682            .collect::<Result<_, _>>()?;
683
684        let mut count = sqlx::QueryBuilder::new("SELECT COUNT(*)");
685        push_expiring_predicates(profile, before, &mut count);
686        let total: i64 = count
687            .build()
688            .fetch_one(&database.pool)
689            .await?
690            .try_get::<i64, _>(0)?;
691
692        Ok((orders, total))
693    }
694
695    /// The issued orders on `profile` whose `cert_not_after` has never been
696    /// derived — rows finalized before the column existed. At most `limit` per
697    /// call, since this parses an X.509 chain per row and the digest that calls
698    /// it is not the only thing the runner has to do.
699    ///
700    /// Returns `(id, chain)` pairs rather than whole orders: the caller wants
701    /// the PEM and nothing else, and a digest running against a long-lived
702    /// deployment would otherwise inflate every row it is about to discard.
703    pub async fn find_unstamped(
704        profile: &str,
705        limit: i64,
706        database: &Database,
707    ) -> Result<Vec<(String, String)>, sqlx::Error> {
708        let rows = sqlx::query(
709            "SELECT id, certificate FROM orders WHERE profile = ? \
710             AND certificate IS NOT NULL AND cert_not_after IS NULL LIMIT ?;",
711        )
712        .bind(profile)
713        .bind(limit)
714        .fetch_all(&database.pool)
715        .await?;
716
717        rows.into_iter()
718            .map(|row| Ok((row.try_get("id")?, row.try_get("certificate")?)))
719            .collect()
720    }
721
722    /// Writes a `cert_not_after` derived after the fact by the sweep.
723    ///
724    /// Separate from [`Order::finalize`] because it is a backfill and not an
725    /// issuance: it touches one column, never `status`, and it is the one
726    /// caller that legitimately writes the negative sentinel — a chain that
727    /// will not parse has to be *recorded* as unparsable, or every pass parses
728    /// it again for the life of the deployment.
729    pub async fn set_cert_not_after(
730        id: &str,
731        cert_not_after: i64,
732        database: &Database,
733    ) -> Result<(), sqlx::Error> {
734        sqlx::query("UPDATE orders SET cert_not_after = ? WHERE id = ?;")
735            .bind(cert_not_after)
736            .bind(id)
737            .execute(&database.pool)
738            .await?;
739        Ok(())
740    }
741
742    /// Looks up the order whose stored certificate carries `serial` (hex,
743    /// matching `cert_serial`'s format) — indexed, so a `POST /revokeCert`
744    /// request is not a full table scan across every issued order. **Not**
745    /// proof of identity on its own: the caller must additionally compare the
746    /// submitted certificate's DER against the returned order's stored chain
747    /// byte-for-byte (a random-serial collision, or a crafted certificate
748    /// reusing a real serial, are not ruled out by the serial alone).
749    ///
750    /// Scoped to `profile`: revocation and ARI carry no account, so the
751    /// endpoint the request arrived at is the only thing that keeps one
752    /// profile from answering for — or revoking — another's certificate.
753    pub async fn find_by_cert_serial(
754        profile: &str,
755        serial: &str,
756        database: &Database,
757    ) -> Result<Option<Order>, sqlx::Error> {
758        debug!(event = "db_order_find_by_cert_serial_started", outcome = "progress", profile = %profile, cert_serial = ?serial);
759        let row = sqlx::query(concat!(
760            "SELECT ",
761            columns!(),
762            " FROM orders WHERE profile = ? AND cert_serial = ?;"
763        ))
764        .bind(profile)
765        .bind(serial)
766        .fetch_optional(&database.pool)
767        .await?;
768
769        let result = row.map(Order::from_row).transpose()?;
770        if result.is_some() {
771            info!(event = "db_order_found_by_cert_serial", outcome = "success", cert_serial = ?serial);
772        } else {
773            debug!(event = "db_order_not_found_by_cert_serial", outcome = "failure", cert_serial = ?serial);
774        }
775        Ok(result)
776    }
777
778    /// Finds any order that already claims to replace `cert_id` and is not
779    /// `invalid` — RFC 9773 §5's "the identified certificate has not already
780    /// been marked as replaced by a different Order that is not `invalid`".
781    ///
782    /// The `invalid` exclusion is what makes a retry work: an order that failed
783    /// validation never produced a replacement, so it must not hold the
784    /// predecessor hostage. Scoped by profile like every other request-path
785    /// lookup — a certID is only meaningful at the endpoint that issued it.
786    pub async fn find_by_replaces(
787        profile: &str,
788        cert_id: &str,
789        database: &Database,
790    ) -> Result<Option<Order>, sqlx::Error> {
791        debug!(event = "db_order_find_by_replaces_started", outcome = "progress", profile = %profile, replaces = %cert_id);
792        let row = sqlx::query(concat!(
793            "SELECT ",
794            columns!(),
795            " FROM orders WHERE profile = ? AND replaces = ? AND status != 'invalid' LIMIT 1;"
796        ))
797        .bind(profile)
798        .bind(cert_id)
799        .fetch_optional(&database.pool)
800        .await?;
801
802        row.map(Order::from_row).transpose()
803    }
804
805    /// Records a certificate revocation (RFC 8555 §7.6): stamps `revoked_at`
806    /// (now) and the optional `CRLReason` `reason`, and keeps `self` in sync
807    /// (like [`Order::finalize`]). Deliberately does **not** touch `status`:
808    /// revocation is orthogonal to the order state machine (RFC 8555 defines
809    /// no "revoked" order status), so a revoked order stays `valid`.
810    pub async fn revoke(
811        &mut self,
812        reason: Option<i64>,
813        database: &Database,
814    ) -> Result<(), sqlx::Error> {
815        let now = now_secs();
816        debug!(event = "db_order_revoke_started", outcome = "progress", order_id = ?self.id, reason = ?reason);
817        sqlx::query("UPDATE orders SET revoked_at = ?, revocation_reason = ? WHERE id = ?;")
818            .bind(now)
819            .bind(reason)
820            .bind(&self.id)
821            .execute(&database.pool)
822            .await?;
823
824        self.revoked_at = Some(now);
825        self.revocation_reason = reason;
826        info!(event = "db_order_revoked", outcome = "success", order_id = ?self.id, reason = ?reason);
827        Ok(())
828    }
829
830    /// Records a failed issuance: stores the `error` problem document, moves the
831    /// order to the terminal `invalid` state, and keeps `self` in sync (like
832    /// [`Order::finalize`]). Used when the signer fails internally; a `badCSR`
833    /// leaves the order `ready` and retryable instead.
834    /// The `invalid` transition as a bare statement, over any executor.
835    ///
836    /// Split from [`Order::mark_invalid`] so `post_challenge` can compose the
837    /// challenge, authorization and order transitions into one transaction. The
838    /// in-memory sync stays in `mark_invalid`, since it must not happen until
839    /// the transaction has committed.
840    pub(crate) async fn set_invalid<'e, E>(
841        id: &str,
842        error: &Value,
843        executor: E,
844    ) -> Result<(), sqlx::Error>
845    where
846        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
847    {
848        // `error` is a `serde_json::Value`, so serialization is infallible.
849        sqlx::query("UPDATE orders SET error = ?, status = 'invalid' WHERE id = ?;")
850            .bind(error.to_string())
851            .bind(id)
852            .execute(executor)
853            .await?;
854        Ok(())
855    }
856
857    /// The `ready` transition as a bare statement; see [`Order::set_invalid`].
858    pub(crate) async fn set_ready<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
859    where
860        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
861    {
862        sqlx::query("UPDATE orders SET status = 'ready' WHERE id = ?;")
863            .bind(id)
864            .execute(executor)
865            .await?;
866        Ok(())
867    }
868
869    /// The `pending` transition as a bare statement; see [`Order::set_invalid`].
870    pub(crate) async fn set_pending<'e, E>(id: &str, executor: E) -> Result<(), sqlx::Error>
871    where
872        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
873    {
874        sqlx::query("UPDATE orders SET status = 'pending' WHERE id = ?;")
875            .bind(id)
876            .execute(executor)
877            .await?;
878        Ok(())
879    }
880
881    pub async fn mark_invalid(
882        &mut self,
883        error: Value,
884        database: &Database,
885    ) -> Result<(), sqlx::Error> {
886        debug!(event = "db_order_mark_invalid_started", outcome = "progress", order_id = ?self.id);
887        Self::set_invalid(&self.id, &error, &database.pool).await?;
888
889        self.error = Some(error);
890        self.status = OrderStatus::Invalid;
891        info!(event = "db_order_marked_invalid", outcome = "failure", order_id = ?self.id);
892        Ok(())
893    }
894
895    /// Moves the order from `pending` to `ready` once all its authorizations are
896    /// `valid`, so it can be finalized. Keeps `self` in sync (like
897    /// [`Order::finalize`]).
898    pub async fn mark_ready(&mut self, database: &Database) -> Result<(), sqlx::Error> {
899        debug!(event = "db_order_mark_ready_started", outcome = "progress", order_id = ?self.id);
900        Self::set_ready(&self.id, &database.pool).await?;
901
902        self.status = OrderStatus::Ready;
903        info!(event = "db_order_marked_ready", outcome = "success", order_id = ?self.id);
904        Ok(())
905    }
906
907    /// Moves the order back from `ready` to `pending`, after one of its
908    /// authorizations stopped being `valid` — in practice, a client
909    /// deactivating one (RFC 8555 §7.5.2).
910    ///
911    /// The only backwards transition in the order state machine, and it exists
912    /// because §7.5.2's "the server MUST NOT treat deactivated authorization
913    /// objects as sufficient for issuing certificates" has to hold for an order
914    /// that already reached `ready` — otherwise `finalize` would still accept
915    /// it. RFC 8555 §7.1.6's diagram draws `pending → ready` as the state
916    /// becoming true rather than a one-way latch, so re-deriving it is in
917    /// keeping with the model.
918    pub async fn mark_pending(&mut self, database: &Database) -> Result<(), sqlx::Error> {
919        debug!(event = "db_order_mark_pending_started", outcome = "progress", order_id = ?self.id);
920        Self::set_pending(&self.id, &database.pool).await?;
921
922        self.status = OrderStatus::Pending;
923        info!(event = "db_order_marked_pending", outcome = "success", order_id = ?self.id);
924        Ok(())
925    }
926
927    /// Claims the order for issuance, moving it from `ready` to `processing` in
928    /// **one guarded statement**: `Ok(true)` means this caller won the claim,
929    /// `Ok(false)` that somebody else already holds it. Keeps `self` in sync
930    /// (like [`Order::mark_ready`]) only on the winning branch.
931    ///
932    /// **The precondition is the whole point.** `post_finalize` reads the order,
933    /// checks it is `ready`, signs, and writes — three steps with no lock
934    /// between them, so N concurrent finalize requests on one order all passed
935    /// the check, all reached `SignerBackend::issue`, and all got a certificate
936    /// back. Only the last write survived, and the others became valid
937    /// CA-signed certificates with no row naming their serial: `POST
938    /// /revokeCert` looks an order up by `find_by_cert_serial` and would answer
939    /// "unknown certificate", so nothing this server offers could ever revoke
940    /// them and the CRL would never learn they exist. `rows_affected` closes it,
941    /// the primitive [`crate::sqlite::nonce::Nonce::verify`] and
942    /// `AdminUser::claim_totp_step` already rest on.
943    ///
944    /// The `relay` backend was never exposed, because `upstream_orders.order_id`
945    /// is a primary key and the second insert conflicts — this gives `local_ca`
946    /// and `custom`, which answer inline, the same guard.
947    ///
948    /// The `processing` status needed no migration — the `orders.status`
949    /// `CHECK` has always allowed it; until the `relay` backend existed
950    /// there was simply no asynchronous issuance to use it.
951    pub async fn claim_for_finalize(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
952        debug!(event = "db_order_mark_processing_started", outcome = "progress", order_id = ?self.id);
953        let claimed = sqlx::query(
954            "UPDATE orders SET status = 'processing' WHERE id = ? AND status = 'ready';",
955        )
956        .bind(&self.id)
957        .execute(&database.pool)
958        .await?
959        .rows_affected()
960            == 1;
961
962        if !claimed {
963            debug!(event = "db_order_finalize_claim_refused", outcome = "failure", order_id = ?self.id);
964            return Ok(false);
965        }
966
967        self.status = OrderStatus::Processing;
968        info!(event = "db_order_marked_processing", outcome = "success", order_id = ?self.id);
969        Ok(true)
970    }
971
972    /// Gives the claim back, moving `processing` to `ready` so the client can
973    /// try again — the counterpart to [`Order::claim_for_finalize`] for the
974    /// refusals RFC 8555 §7.4 says must leave the order finalizable (a rejected
975    /// CSR) and for the two arms where issuance succeeded but this server could
976    /// not read what it had just been handed.
977    ///
978    /// Guarded on `processing` for a reason of its own: a §7.5.2 deactivation
979    /// racing this claim demotes the order to `pending`
980    /// ([`Order::mark_pending`], unguarded, since it is the authoritative
981    /// answer to an authorization that stopped being valid). An unguarded
982    /// release would push it back to `ready` and hand the client a finalizable
983    /// order whose authorizations no longer support it.
984    pub async fn release_finalize_claim(&mut self, database: &Database) -> Result<(), sqlx::Error> {
985        let released = sqlx::query(
986            "UPDATE orders SET status = 'ready' WHERE id = ? AND status = 'processing';",
987        )
988        .bind(&self.id)
989        .execute(&database.pool)
990        .await?
991        .rows_affected()
992            == 1;
993
994        if released {
995            self.status = OrderStatus::Ready;
996        }
997        debug!(event = "db_order_finalize_claim_released", outcome = "success", order_id = ?self.id, released = released);
998        Ok(())
999    }
1000
1001    /// The RFC 8555 order object. URLs are derived from `base_url`; datetimes are
1002    /// rendered RFC3339. `authorizations` lists one URL per `authz_ids` entry, the
1003    /// `certificate` URL appears only once the order is `valid`, and `notBefore`/
1004    /// `notAfter`/`error` appear only when set.
1005    #[must_use]
1006    pub fn to_json(&self, base_url: &str, authz_ids: &[String]) -> Value {
1007        let mut object = serde_json::Map::new();
1008        object.insert(
1009            "status".to_string(),
1010            Value::String(self.status.as_str().to_string()),
1011        );
1012        object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
1013        object.insert(
1014            "identifiers".to_string(),
1015            serde_json::to_value(&self.identifiers).expect("Identifier is always serializable"),
1016        );
1017        if let Some(nb) = self.not_before {
1018            object.insert("notBefore".to_string(), Value::String(rfc3339(nb)));
1019        }
1020        if let Some(na) = self.not_after {
1021            object.insert("notAfter".to_string(), Value::String(rfc3339(na)));
1022        }
1023        let authorizations: Vec<Value> = authz_ids
1024            .iter()
1025            .map(|id| Value::String(format!("{base_url}/authz/{id}")))
1026            .collect();
1027        object.insert("authorizations".to_string(), Value::Array(authorizations));
1028        object.insert(
1029            "finalize".to_string(),
1030            Value::String(format!("{base_url}/order/{}/finalize", self.id)),
1031        );
1032        if self.status == OrderStatus::Valid {
1033            object.insert(
1034                "certificate".to_string(),
1035                Value::String(format!("{base_url}/certificate/{}", self.id)),
1036            );
1037        }
1038        if let Some(ref error) = self.error {
1039            object.insert("error".to_string(), error.clone());
1040        }
1041        // RFC 9773 §5: the field is reflected "in the response and in
1042        // subsequent requests for the corresponding Order object" — so it is
1043        // rendered here, not just echoed once on the 201.
1044        if let Some(ref replaces) = self.replaces {
1045            object.insert("replaces".to_string(), Value::String(replaces.clone()));
1046        }
1047        Value::Object(object)
1048    }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053    use super::*;
1054    use crate::audit::ClientContext;
1055
1056    /// `with_client` is set between `new` and `insert`, the way `replaces` is,
1057    /// and — like `replaces` — it has to survive the round trip. An order built
1058    /// without it keeps two `NULL`s rather than empty strings.
1059    #[tokio::test]
1060    async fn with_client_persists_and_an_order_without_one_stays_null() {
1061        let db = std::sync::Arc::new(Database::connect_in_memory().await.unwrap());
1062        let account = account_id(&db).await;
1063
1064        let stamped = Order::new(
1065            "default",
1066            &account,
1067            vec![Identifier::dns("a.example.com")],
1068            0,
1069            None,
1070            None,
1071        )
1072        .with_client(&ClientContext {
1073            ip: Some("203.0.113.7".to_string()),
1074            ptr: Some("host.example.com".to_string()),
1075            user_agent: Some("lego".to_string()),
1076            request_id: Some("req-1".to_string()),
1077        });
1078        stamped.insert(&db.pool).await.unwrap();
1079        let reloaded = Order::find_by_id(&stamped.id, &db).await.unwrap().unwrap();
1080        assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
1081        assert_eq!(reloaded.created_ptr.as_deref(), Some("host.example.com"));
1082
1083        let bare = Order::new(
1084            "default",
1085            &account,
1086            vec![Identifier::dns("b.example.com")],
1087            0,
1088            None,
1089            None,
1090        );
1091        bare.insert(&db.pool).await.unwrap();
1092        let reloaded = Order::find_by_id(&bare.id, &db).await.unwrap().unwrap();
1093        assert_eq!(reloaded.created_ip, None);
1094        assert_eq!(reloaded.created_ptr, None);
1095
1096        // And the ACME order object says nothing about either: RFC 8555 §7.1.3
1097        // defines its members, and where a client connected from is not one.
1098        let json = reloaded.to_json("http://localhost:3000", &[]);
1099        let object = json.as_object().unwrap();
1100        assert!(!object.contains_key("createdIp"));
1101        assert!(!object.contains_key("createdPtr"));
1102        assert!(
1103            !stamped
1104                .to_json("http://localhost:3000", &[])
1105                .to_string()
1106                .contains("203.0.113.7")
1107        );
1108    }
1109
1110    use crate::testutil::account_id;
1111    use serde_json::json;
1112    use std::sync::Arc;
1113
1114    #[tokio::test]
1115    async fn create_then_find_by_id_round_trip() {
1116        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1117        let acct = account_id(&db).await;
1118
1119        let created = Order::create(
1120            "default",
1121            &acct,
1122            vec![Identifier::dns("example.com")],
1123            now_secs() + 3600,
1124            None,
1125            None,
1126            &db,
1127        )
1128        .await
1129        .unwrap();
1130        assert_eq!(created.status, OrderStatus::Pending);
1131
1132        let found = Order::find_by_id(&created.id, &db).await.unwrap().unwrap();
1133        assert_eq!(found.account_id, acct);
1134        assert_eq!(found.identifiers, vec![Identifier::dns("example.com")]);
1135        assert!(found.certificate.is_none());
1136    }
1137
1138    #[tokio::test]
1139    async fn find_by_account_lists_all() {
1140        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1141        let acct = account_id(&db).await;
1142
1143        Order::create(
1144            "default",
1145            &acct,
1146            vec![Identifier::dns("a.example")],
1147            now_secs() + 3600,
1148            None,
1149            None,
1150            &db,
1151        )
1152        .await
1153        .unwrap();
1154        Order::create(
1155            "default",
1156            &acct,
1157            vec![Identifier::dns("b.example")],
1158            now_secs() + 3600,
1159            None,
1160            None,
1161            &db,
1162        )
1163        .await
1164        .unwrap();
1165
1166        let orders = Order::find_by_account(&acct, &db).await.unwrap();
1167        assert_eq!(orders.len(), 2);
1168    }
1169
1170    #[tokio::test]
1171    async fn absent_lookup_returns_none() {
1172        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1173        assert!(Order::find_by_id("nope", &db).await.unwrap().is_none());
1174    }
1175
1176    #[tokio::test]
1177    async fn to_json_shape_when_pending() {
1178        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1179        let acct = account_id(&db).await;
1180
1181        let order = Order::create(
1182            "default",
1183            &acct,
1184            vec![Identifier::dns("example.com")],
1185            now_secs() + 3600,
1186            None,
1187            None,
1188            &db,
1189        )
1190        .await
1191        .unwrap();
1192
1193        let authz_ids = vec!["authz-1".to_string()];
1194        let json = order.to_json("http://localhost:3000", &authz_ids);
1195        assert_eq!(json["status"], "pending");
1196        assert_eq!(
1197            json["authorizations"],
1198            json!(["http://localhost:3000/authz/authz-1"])
1199        );
1200        assert_eq!(
1201            json["finalize"],
1202            format!("http://localhost:3000/order/{}/finalize", order.id)
1203        );
1204        assert_eq!(
1205            json["identifiers"],
1206            json!([{"type": "dns", "value": "example.com"}])
1207        );
1208        // A pending order has no certificate URL yet, and no notBefore/notAfter.
1209        assert!(json.get("certificate").is_none());
1210        assert!(json.get("notBefore").is_none());
1211        assert!(json.get("notAfter").is_none());
1212        // `expires` renders as an RFC3339 string ending in Z.
1213        assert!(json["expires"].as_str().unwrap().ends_with('Z'));
1214    }
1215
1216    #[tokio::test]
1217    async fn to_json_includes_optional_fields() {
1218        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1219        let acct = account_id(&db).await;
1220
1221        let order = Order::create(
1222            "default",
1223            &acct,
1224            vec![Identifier::dns("example.com")],
1225            now_secs() + 3600,
1226            Some(now_secs()),
1227            Some(now_secs() + 7200),
1228            &db,
1229        )
1230        .await
1231        .unwrap();
1232
1233        let json = order.to_json("http://localhost:3000", &[]);
1234        assert!(json["notBefore"].as_str().unwrap().ends_with('Z'));
1235        assert!(json["notAfter"].as_str().unwrap().ends_with('Z'));
1236    }
1237
1238    #[tokio::test]
1239    async fn finalize_persists_and_syncs() {
1240        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1241        let acct = account_id(&db).await;
1242
1243        let mut order = Order::create(
1244            "default",
1245            &acct,
1246            vec![Identifier::dns("example.com")],
1247            now_secs() + 3600,
1248            None,
1249            None,
1250            &db,
1251        )
1252        .await
1253        .unwrap();
1254
1255        order
1256            .finalize(
1257                "-----BEGIN CERTIFICATE-----\n...".to_string(),
1258                "aabbcc".to_string(),
1259                vec![1, 2, 3],
1260                Some(now_secs() + 90 * 24 * 60 * 60),
1261                &db,
1262            )
1263            .await
1264            .unwrap();
1265
1266        // In-memory struct is updated…
1267        assert_eq!(order.status, OrderStatus::Valid);
1268        assert!(order.certificate.is_some());
1269        assert_eq!(order.cert_serial.as_deref(), Some("aabbcc"));
1270        assert_eq!(order.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1271        // …and so is the stored row, and to_json now exposes the certificate URL.
1272        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1273        assert_eq!(reloaded.status, OrderStatus::Valid);
1274        assert_eq!(reloaded.cert_serial.as_deref(), Some("aabbcc"));
1275        assert_eq!(reloaded.cert_pubkey.as_deref(), Some(&[1u8, 2, 3][..]));
1276        assert!(reloaded.cert_not_after.is_some());
1277        let json = reloaded.to_json("http://localhost:3000", &[]);
1278        assert_eq!(
1279            json["certificate"],
1280            format!("http://localhost:3000/certificate/{}", order.id)
1281        );
1282    }
1283
1284    /// The guard the whole double-issuance fix rests on: two callers race, and
1285    /// exactly one of them may go on to ask a signer for a certificate.
1286    #[tokio::test]
1287    async fn only_one_caller_can_claim_an_order_for_finalize() {
1288        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1289        let acct = account_id(&db).await;
1290
1291        let mut order = Order::create(
1292            "default",
1293            &acct,
1294            vec![Identifier::dns("example.com")],
1295            now_secs() + 3600,
1296            None,
1297            None,
1298            &db,
1299        )
1300        .await
1301        .unwrap();
1302        order.mark_ready(&db).await.unwrap();
1303
1304        // A second handle on the same row, as a concurrent request would have.
1305        let mut rival = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1306
1307        assert!(order.claim_for_finalize(&db).await.unwrap());
1308        assert_eq!(order.status, OrderStatus::Processing);
1309
1310        // The loser is told so, and its own in-memory copy is left alone rather
1311        // than being synced to a status it does not hold the claim on.
1312        assert!(!rival.claim_for_finalize(&db).await.unwrap());
1313        assert_eq!(rival.status, OrderStatus::Ready);
1314
1315        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1316        assert_eq!(reloaded.status, OrderStatus::Processing);
1317    }
1318
1319    /// Every status but `ready` refuses the claim — `pending` because the
1320    /// authorizations do not support issuance yet, `valid` because a
1321    /// certificate already exists, `invalid` because the order is terminal.
1322    #[tokio::test]
1323    async fn an_order_that_is_not_ready_cannot_be_claimed() {
1324        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1325        let acct = account_id(&db).await;
1326
1327        for prepare in [
1328            // `pending` is the state `create` leaves behind.
1329            None,
1330            Some(OrderStatus::Valid),
1331            Some(OrderStatus::Invalid),
1332        ] {
1333            let mut order = Order::create(
1334                "default",
1335                &acct,
1336                vec![Identifier::dns("example.com")],
1337                now_secs() + 3600,
1338                None,
1339                None,
1340                &db,
1341            )
1342            .await
1343            .unwrap();
1344            match prepare {
1345                None => {}
1346                Some(OrderStatus::Valid) => order
1347                    .finalize("chain".to_string(), "aa".to_string(), vec![1], None, &db)
1348                    .await
1349                    .unwrap(),
1350                Some(_) => order
1351                    .mark_invalid(serde_json::json!({}), &db)
1352                    .await
1353                    .unwrap(),
1354            }
1355            let before = order.status;
1356
1357            assert!(
1358                !order.claim_for_finalize(&db).await.unwrap(),
1359                "claimed an order in {before}"
1360            );
1361            assert_eq!(order.status, before);
1362        }
1363    }
1364
1365    /// The release is the counterpart RFC 8555 §7.4 needs for a rejected CSR,
1366    /// and it is guarded so a §7.5.2 deactivation racing it wins.
1367    #[tokio::test]
1368    async fn releasing_a_claim_restores_ready_but_never_overrides_a_demotion() {
1369        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1370        let acct = account_id(&db).await;
1371
1372        let mut order = Order::create(
1373            "default",
1374            &acct,
1375            vec![Identifier::dns("example.com")],
1376            now_secs() + 3600,
1377            None,
1378            None,
1379            &db,
1380        )
1381        .await
1382        .unwrap();
1383        order.mark_ready(&db).await.unwrap();
1384        assert!(order.claim_for_finalize(&db).await.unwrap());
1385
1386        order.release_finalize_claim(&db).await.unwrap();
1387        assert_eq!(order.status, OrderStatus::Ready);
1388        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1389        assert_eq!(reloaded.status, OrderStatus::Ready);
1390
1391        // Now the race: the claim is held, a deactivation demotes the order,
1392        // and the release must not hand the client back a finalizable order
1393        // whose authorizations no longer support it.
1394        assert!(order.claim_for_finalize(&db).await.unwrap());
1395        order.mark_pending(&db).await.unwrap();
1396        order.release_finalize_claim(&db).await.unwrap();
1397        assert_eq!(order.status, OrderStatus::Pending);
1398        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1399        assert_eq!(reloaded.status, OrderStatus::Pending);
1400    }
1401
1402    async fn finalized_order(db: Arc<Database>, serial: &str) -> Order {
1403        let acct = account_id(&db).await;
1404        let mut order = Order::create(
1405            "default",
1406            &acct,
1407            vec![Identifier::dns("example.com")],
1408            now_secs() + 3600,
1409            None,
1410            None,
1411            &db,
1412        )
1413        .await
1414        .unwrap();
1415        order
1416            .finalize(
1417                "-----BEGIN CERTIFICATE-----\n...".to_string(),
1418                serial.to_string(),
1419                vec![9, 9, 9],
1420                None,
1421                &db,
1422            )
1423            .await
1424            .unwrap();
1425        order
1426    }
1427
1428    #[tokio::test]
1429    async fn find_by_cert_serial_round_trip() {
1430        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1431        let order = finalized_order(db.clone(), "deadbeef").await;
1432
1433        let found = Order::find_by_cert_serial("default", "deadbeef", &db)
1434            .await
1435            .unwrap()
1436            .unwrap();
1437        assert_eq!(found.id, order.id);
1438
1439        assert!(
1440            Order::find_by_cert_serial("default", "unknown", &db)
1441                .await
1442                .unwrap()
1443                .is_none()
1444        );
1445    }
1446
1447    #[tokio::test]
1448    async fn revoke_persists_and_syncs() {
1449        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1450        let mut order = finalized_order(db.clone(), "aa11bb22").await;
1451
1452        order.revoke(Some(1), &db).await.unwrap();
1453
1454        // In-memory struct is updated, and `status` is untouched…
1455        assert!(order.revoked_at.is_some());
1456        assert_eq!(order.revocation_reason, Some(1));
1457        assert_eq!(order.status, OrderStatus::Valid);
1458        // …and so is the stored row.
1459        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1460        assert!(reloaded.revoked_at.is_some());
1461        assert_eq!(reloaded.revocation_reason, Some(1));
1462        assert_eq!(reloaded.status, OrderStatus::Valid);
1463    }
1464
1465    #[tokio::test]
1466    async fn revoke_with_no_reason_persists_null() {
1467        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1468        let mut order = finalized_order(db.clone(), "cc33dd44").await;
1469
1470        order.revoke(None, &db).await.unwrap();
1471
1472        assert!(order.revoked_at.is_some());
1473        assert!(order.revocation_reason.is_none());
1474        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1475        assert!(reloaded.revocation_reason.is_none());
1476    }
1477
1478    #[tokio::test]
1479    async fn to_json_never_exposes_revocation_state() {
1480        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1481        let mut order = finalized_order(db.clone(), "ee55ff66").await;
1482        order.revoke(Some(1), &db).await.unwrap();
1483
1484        let json = order.to_json("http://localhost:3000", &[]);
1485        assert!(json.get("revokedAt").is_none());
1486        assert!(json.get("revocationReason").is_none());
1487        assert_eq!(json["status"], "valid");
1488    }
1489
1490    #[tokio::test]
1491    async fn mark_invalid_persists_and_syncs() {
1492        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1493        let acct = account_id(&db).await;
1494
1495        let mut order = Order::create(
1496            "default",
1497            &acct,
1498            vec![Identifier::dns("example.com")],
1499            now_secs() + 3600,
1500            None,
1501            None,
1502            &db,
1503        )
1504        .await
1505        .unwrap();
1506
1507        let error = json!({
1508            "type": "urn:ietf:params:acme:error:serverInternal",
1509            "detail": "boom",
1510            "status": 500,
1511        });
1512        order.mark_invalid(error.clone(), &db).await.unwrap();
1513
1514        // In-memory struct is updated…
1515        assert_eq!(order.status, OrderStatus::Invalid);
1516        assert_eq!(order.error, Some(error.clone()));
1517        // …and so is the stored row, and to_json now exposes the error object.
1518        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
1519        assert_eq!(reloaded.status, OrderStatus::Invalid);
1520        let json = reloaded.to_json("http://localhost:3000", &[]);
1521        assert_eq!(json["error"], error);
1522    }
1523
1524    #[tokio::test]
1525    async fn delete_removes_the_row_and_reports_true() {
1526        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1527        let acct = account_id(&db).await;
1528        let order = Order::create(
1529            "default",
1530            &acct,
1531            vec![Identifier::dns("example.com")],
1532            now_secs() + 3600,
1533            None,
1534            None,
1535            &db,
1536        )
1537        .await
1538        .unwrap();
1539
1540        assert!(Order::delete(&order.id, &db).await.unwrap());
1541        assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
1542    }
1543
1544    #[tokio::test]
1545    async fn delete_of_unknown_id_reports_false() {
1546        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1547        assert!(!Order::delete("nope", &db).await.unwrap());
1548    }
1549
1550    #[tokio::test]
1551    async fn delete_cascades_to_authorizations_and_challenges() {
1552        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1553        let acct = account_id(&db).await;
1554        let order = Order::create(
1555            "default",
1556            &acct,
1557            vec![Identifier::dns("example.com")],
1558            now_secs() + 3600,
1559            None,
1560            None,
1561            &db,
1562        )
1563        .await
1564        .unwrap();
1565
1566        let authz = crate::sqlite::authz::Authorization::create(
1567            &order.id,
1568            Identifier::dns("example.com"),
1569            now_secs() + 3600,
1570            &db,
1571        )
1572        .await
1573        .unwrap();
1574        crate::sqlite::authz::Challenge::create(&authz.id, "http-01", &db)
1575            .await
1576            .unwrap();
1577
1578        Order::delete(&order.id, &db).await.unwrap();
1579
1580        assert!(
1581            crate::sqlite::authz::Authorization::find_by_order(&order.id, &db)
1582                .await
1583                .unwrap()
1584                .is_empty()
1585        );
1586        assert!(
1587            crate::sqlite::authz::Challenge::find_by_authz(&authz.id, &db)
1588                .await
1589                .unwrap()
1590                .is_empty()
1591        );
1592    }
1593
1594    /// Seeds `count` orders under `profile`, each backdated one second further
1595    /// than the last so `created_at DESC` has a deterministic answer without
1596    /// relying on the UUID tiebreak.
1597    async fn seed_orders(
1598        db: &Arc<Database>,
1599        profile: &str,
1600        account_id: &str,
1601        count: usize,
1602    ) -> Vec<String> {
1603        let base = now_secs();
1604        let mut ids = Vec::new();
1605        for index in 0..count {
1606            let order = Order::create(
1607                profile,
1608                account_id,
1609                vec![Identifier::dns(format!("host-{index}.example.com"))],
1610                base + 3600,
1611                None,
1612                None,
1613                db,
1614            )
1615            .await
1616            .unwrap();
1617            sqlx::query("UPDATE orders SET created_at = ? WHERE id = ?;")
1618                .bind(base - index as i64)
1619                .bind(&order.id)
1620                .execute(&db.pool)
1621                .await
1622                .unwrap();
1623            ids.push(order.id);
1624        }
1625        // Newest first is the order `search` returns, and `ids` is already in
1626        // that order: index 0 was backdated least.
1627        ids
1628    }
1629
1630    fn window(limit: i64, offset: i64) -> OrderQuery {
1631        OrderQuery {
1632            limit,
1633            offset,
1634            ..OrderQuery::default()
1635        }
1636    }
1637
1638    #[tokio::test]
1639    async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1640        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1641        let acct = account_id(&db).await;
1642        let ids = seed_orders(&db, "default", &acct, 5).await;
1643
1644        let (page, total) = Order::search(&window(2, 0), &db).await.unwrap();
1645        assert_eq!(total, 5, "the total must ignore the page window");
1646        assert_eq!(
1647            page.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
1648            ids[..2]
1649        );
1650
1651        let (second, total) = Order::search(&window(2, 2), &db).await.unwrap();
1652        assert_eq!(total, 5);
1653        assert_eq!(
1654            second.iter().map(|o| o.id.clone()).collect::<Vec<_>>(),
1655            ids[2..4]
1656        );
1657
1658        // A partial last page, then past the end.
1659        let (last, _) = Order::search(&window(2, 4), &db).await.unwrap();
1660        assert_eq!(last.len(), 1);
1661        let (beyond, total) = Order::search(&window(2, 99), &db).await.unwrap();
1662        assert!(beyond.is_empty());
1663        assert_eq!(total, 5, "a page past the end still reports the real total");
1664    }
1665
1666    /// Every page must be disjoint and together cover everything: the property
1667    /// the `created_at, id` tiebreak exists for.
1668    #[tokio::test]
1669    async fn paging_one_row_at_a_time_sees_every_order_exactly_once() {
1670        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1671        let acct = account_id(&db).await;
1672        // Deliberately NOT backdated: all four share one `created_at`, which is
1673        // exactly the case where a missing tiebreak lets rows swap pages.
1674        let mut expected = Vec::new();
1675        for index in 0..4 {
1676            let order = Order::create(
1677                "default",
1678                &acct,
1679                vec![Identifier::dns(format!("same-second-{index}.example.com"))],
1680                now_secs() + 3600,
1681                None,
1682                None,
1683                &db,
1684            )
1685            .await
1686            .unwrap();
1687            expected.push(order.id);
1688        }
1689        expected.sort();
1690
1691        let mut seen = Vec::new();
1692        for offset in 0..4 {
1693            let (page, total) = Order::search(&window(1, offset), &db).await.unwrap();
1694            assert_eq!(total, 4);
1695            assert_eq!(page.len(), 1);
1696            seen.push(page[0].id.clone());
1697        }
1698        seen.sort();
1699        assert_eq!(
1700            seen, expected,
1701            "pages must be disjoint and cover everything"
1702        );
1703    }
1704
1705    #[tokio::test]
1706    async fn search_filters_by_profile_account_and_status_together() {
1707        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1708        let acct = account_id(&db).await;
1709        let (other_account, _) = crate::sqlite::account::Account::find_or_create(
1710            "default",
1711            &[9u8, 9, 9],
1712            vec![],
1713            &ClientContext::default(),
1714            &db,
1715        )
1716        .await
1717        .unwrap();
1718
1719        seed_orders(&db, "default", &acct, 3).await;
1720        seed_orders(&db, "default", &other_account.id, 2).await;
1721        let mut ready = seed_orders(&db, "default", &acct, 1).await;
1722        let ready_id = ready.pop().unwrap();
1723        Order::find_by_id(&ready_id, &db)
1724            .await
1725            .unwrap()
1726            .unwrap()
1727            .mark_ready(&db)
1728            .await
1729            .unwrap();
1730
1731        // No filter at all: everything.
1732        let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1733        assert_eq!(total, 6);
1734
1735        // By account.
1736        let by_account = OrderQuery {
1737            account_id: Some(acct.clone()),
1738            ..window(50, 0)
1739        };
1740        let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1741        assert_eq!(total, 4);
1742        assert!(rows.iter().all(|o| o.account_id == acct));
1743
1744        // By status.
1745        let by_status = OrderQuery {
1746            status: Some(OrderStatus::Ready),
1747            ..window(50, 0)
1748        };
1749        let (rows, total) = Order::search(&by_status, &db).await.unwrap();
1750        assert_eq!(total, 1);
1751        assert_eq!(rows[0].id, ready_id);
1752
1753        // All three at once, and the count must agree with the rows.
1754        let combined = OrderQuery {
1755            profile: Some("default".to_string()),
1756            account_id: Some(acct.clone()),
1757            status: Some(OrderStatus::Pending),
1758            limit: 50,
1759            offset: 0,
1760        };
1761        let (rows, total) = Order::search(&combined, &db).await.unwrap();
1762        assert_eq!(rows.len(), 3);
1763        assert_eq!(total, 3);
1764
1765        // A filter matching nothing is empty, not an error.
1766        let none = OrderQuery {
1767            profile: Some("no-such-profile".to_string()),
1768            ..window(50, 0)
1769        };
1770        let (rows, total) = Order::search(&none, &db).await.unwrap();
1771        assert!(rows.is_empty());
1772        assert_eq!(total, 0);
1773    }
1774
1775    #[tokio::test]
1776    async fn search_scopes_by_profile() {
1777        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1778        let acct = account_id(&db).await;
1779        seed_orders(&db, "default", &acct, 2).await;
1780        seed_orders(&db, "other", &acct, 3).await;
1781
1782        let scoped = OrderQuery {
1783            profile: Some("other".to_string()),
1784            ..window(50, 0)
1785        };
1786        let (rows, total) = Order::search(&scoped, &db).await.unwrap();
1787        assert_eq!(total, 3);
1788        assert!(rows.iter().all(|o| o.profile == "other"));
1789    }
1790
1791    /// A value that would be SQL if it were interpolated instead of bound.
1792    ///
1793    /// `status` used to be the vector here, and is no longer expressible: it is
1794    /// an [`OrderStatus`], so a hostile value cannot reach this layer at all —
1795    /// `Order::search` never sees one, because `--status` and `?status=` refuse
1796    /// it by name first. `profile` and `account_id` are still free strings and
1797    /// still go through `push_bind`, so the property is asserted on those.
1798    #[tokio::test]
1799    async fn a_filter_value_is_bound_not_interpolated() {
1800        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1801        let acct = account_id(&db).await;
1802        seed_orders(&db, "default", &acct, 2).await;
1803
1804        for hostile in ["' OR 1=1 --", "default'; DROP TABLE orders; --"] {
1805            let by_profile = OrderQuery {
1806                profile: Some(hostile.to_string()),
1807                ..window(50, 0)
1808            };
1809            let (rows, total) = Order::search(&by_profile, &db).await.unwrap();
1810            assert!(rows.is_empty(), "the value must be compared, not executed");
1811            assert_eq!(total, 0);
1812
1813            let by_account = OrderQuery {
1814                account_id: Some(hostile.to_string()),
1815                ..window(50, 0)
1816            };
1817            let (rows, total) = Order::search(&by_account, &db).await.unwrap();
1818            assert!(rows.is_empty(), "the value must be compared, not executed");
1819            assert_eq!(total, 0);
1820        }
1821
1822        // The table is still there, which is what the second vector is for.
1823        let (_, total) = Order::search(&window(50, 0), &db).await.unwrap();
1824        assert_eq!(total, 2);
1825    }
1826
1827    /// A helper for the expiry suite: an issued order whose leaf expires at
1828    /// `not_after`, written through `finalize` so the row's shape is the
1829    /// production one and only the date is a fixture.
1830    async fn expiring_order(
1831        db: &Database,
1832        account: &str,
1833        names: &[&str],
1834        not_after: Option<i64>,
1835    ) -> Order {
1836        expiring_order_on(db, "default", account, names, not_after).await
1837    }
1838
1839    /// [`expiring_order`] on a named endpoint, for the cases about scoping.
1840    async fn expiring_order_on(
1841        db: &Database,
1842        profile: &str,
1843        account: &str,
1844        names: &[&str],
1845        not_after: Option<i64>,
1846    ) -> Order {
1847        let mut order = Order::create(
1848            profile,
1849            account,
1850            names.iter().map(|name| Identifier::dns(*name)).collect(),
1851            now_secs() + 3600,
1852            None,
1853            None,
1854            db,
1855        )
1856        .await
1857        .unwrap();
1858        order
1859            .finalize(
1860                "-----BEGIN CERTIFICATE-----\n...".to_string(),
1861                format!("serial-{}", &order.id[..8]),
1862                vec![1],
1863                not_after,
1864                db,
1865            )
1866            .await
1867            .unwrap();
1868        order
1869    }
1870
1871    const DAY: i64 = 24 * 60 * 60;
1872
1873    /// The window and the ordering: soonest first, and nothing past the
1874    /// horizon.
1875    #[tokio::test]
1876    async fn find_expiring_returns_the_window_soonest_first() {
1877        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1878        let acct = account_id(&db).await;
1879        let now = now_secs();
1880
1881        let far = expiring_order(&db, &acct, &["far.example.com"], Some(now + 60 * DAY)).await;
1882        let soon = expiring_order(&db, &acct, &["soon.example.com"], Some(now + 2 * DAY)).await;
1883        let mid = expiring_order(&db, &acct, &["mid.example.com"], Some(now + 9 * DAY)).await;
1884
1885        let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
1886            .await
1887            .unwrap();
1888
1889        let ids: Vec<&str> = page.iter().map(|order| order.id.as_str()).collect();
1890        assert_eq!(ids, vec![soon.id.as_str(), mid.id.as_str()]);
1891        assert_eq!(total, 2);
1892        assert!(
1893            !ids.contains(&far.id.as_str()),
1894            "a certificate outside the window is not expiring yet"
1895        );
1896    }
1897
1898    /// The three rows the digest must never report, each for its own reason.
1899    #[tokio::test]
1900    async fn find_expiring_skips_revoked_unstamped_and_unparsable_rows() {
1901        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1902        let acct = account_id(&db).await;
1903        let now = now_secs();
1904
1905        let live = expiring_order(&db, &acct, &["live.example.com"], Some(now + DAY)).await;
1906
1907        // Revoked: withdrawn, so not something to go and renew.
1908        let mut revoked =
1909            expiring_order(&db, &acct, &["revoked.example.com"], Some(now + DAY)).await;
1910        revoked.revoke(Some(1), &db).await.unwrap();
1911
1912        // Never stamped: issued before the column existed. The backfill has to
1913        // reach it before the digest can, or a NULL would sort as "expiring".
1914        expiring_order(&db, &acct, &["old.example.com"], None).await;
1915
1916        // Stamped unparsable: the sweep looked and could not read the chain.
1917        // A negative value must not read as "expired in 1970".
1918        let broken = expiring_order(&db, &acct, &["broken.example.com"], None).await;
1919        Order::set_cert_not_after(&broken.id, -1, &db)
1920            .await
1921            .unwrap();
1922
1923        let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 10, 0, &db)
1924            .await
1925            .unwrap();
1926        let ids: Vec<&str> = page.iter().map(|order| order.id.as_str()).collect();
1927        assert_eq!(ids, vec![live.id.as_str()]);
1928        assert_eq!(total, 1);
1929    }
1930
1931    /// The total is counted, not derived from the page — which is the whole
1932    /// reason `max_entries` can truncate a digest and it can still say how many
1933    /// it did not name.
1934    #[tokio::test]
1935    async fn find_expiring_reports_the_unpaged_total() {
1936        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1937        let acct = account_id(&db).await;
1938        let now = now_secs();
1939        for index in 0..5 {
1940            let name = format!("host-{index}.example.com");
1941            expiring_order(&db, &acct, &[name.as_str()], Some(now + DAY)).await;
1942        }
1943
1944        let (page, total) = Order::find_expiring(Some("default"), now + 14 * DAY, 2, 0, &db)
1945            .await
1946            .unwrap();
1947        assert_eq!(page.len(), 2);
1948        assert_eq!(total, 5);
1949    }
1950
1951    /// One endpoint never reports another's certificates — `find_by_cert_serial`'s
1952    /// rule, and for the same reason.
1953    #[tokio::test]
1954    async fn find_expiring_scopes_by_profile() {
1955        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1956        let acct = account_id(&db).await;
1957        let now = now_secs();
1958        expiring_order(&db, &acct, &["a.example.com"], Some(now + DAY)).await;
1959
1960        let (page, total) = Order::find_expiring(Some("other"), now + 14 * DAY, 10, 0, &db)
1961            .await
1962            .unwrap();
1963        assert!(page.is_empty());
1964        assert_eq!(total, 0);
1965    }
1966
1967    /// The panel's default view: no profile, so every endpoint at once. The
1968    /// digest never asks this — it iterates its configured profiles — but the
1969    /// admin surfaces open on it.
1970    #[tokio::test]
1971    async fn find_expiring_unscoped_spans_every_profile() {
1972        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1973        let acct = account_id(&db).await;
1974        let now = now_secs();
1975
1976        let here =
1977            expiring_order_on(&db, "default", &acct, &["a.example.com"], Some(now + DAY)).await;
1978        let there =
1979            expiring_order_on(&db, "other", &acct, &["b.example.com"], Some(now + 2 * DAY)).await;
1980
1981        let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
1982            .await
1983            .unwrap();
1984        let ids: Vec<&str> = page.iter().map(|order| order.id.as_str()).collect();
1985        assert_eq!(ids, vec![here.id.as_str(), there.id.as_str()]);
1986        assert_eq!(total, 2);
1987
1988        // And the three unconditional predicates still apply unscoped: a
1989        // revoked row is absent whichever endpoint issued it.
1990        let mut revoked =
1991            expiring_order_on(&db, "other", &acct, &["c.example.com"], Some(now + DAY)).await;
1992        revoked.revoke(Some(1), &db).await.unwrap();
1993        let (page, total) = Order::find_expiring(None, now + 14 * DAY, 10, 0, &db)
1994            .await
1995            .unwrap();
1996        assert_eq!(page.len(), 2);
1997        assert_eq!(total, 2);
1998    }
1999
2000    /// The offset the page control needs: consecutive windows do not overlap,
2001    /// and the total stays the *unpaged* count so the pager's arithmetic has
2002    /// something honest to work from.
2003    #[tokio::test]
2004    async fn find_expiring_pages_without_overlap_and_keeps_the_unpaged_total() {
2005        let db = Arc::new(Database::connect_in_memory().await.unwrap());
2006        let acct = account_id(&db).await;
2007        let now = now_secs();
2008        for index in 0..5 {
2009            let name = format!("host-{index}.example.com");
2010            // Distinct expiries, so the ordering is total and the assertion
2011            // below is about the offset rather than about a tie-break.
2012            expiring_order(&db, &acct, &[name.as_str()], Some(now + (index + 1) * DAY)).await;
2013        }
2014
2015        let (first, total) = Order::find_expiring(None, now + 14 * DAY, 2, 0, &db)
2016            .await
2017            .unwrap();
2018        let (second, second_total) = Order::find_expiring(None, now + 14 * DAY, 2, 2, &db)
2019            .await
2020            .unwrap();
2021
2022        assert_eq!(total, 5);
2023        assert_eq!(second_total, 5, "the total is unpaged on every window");
2024        assert_eq!(first.len(), 2);
2025        assert_eq!(second.len(), 2);
2026        let firsts: Vec<&str> = first.iter().map(|order| order.id.as_str()).collect();
2027        for order in &second {
2028            assert!(
2029                !firsts.contains(&order.id.as_str()),
2030                "a row must not appear on two pages"
2031            );
2032        }
2033
2034        // Past the end is an empty page, not an error and not a wrapped one.
2035        let (past, _) = Order::find_expiring(None, now + 14 * DAY, 2, 50, &db)
2036            .await
2037            .unwrap();
2038        assert!(past.is_empty());
2039    }
2040
2041    /// The backfill's input: rows with a chain and no stamp, and nothing else.
2042    #[tokio::test]
2043    async fn find_unstamped_finds_only_issued_rows_with_no_stamp() {
2044        let db = Arc::new(Database::connect_in_memory().await.unwrap());
2045        let acct = account_id(&db).await;
2046
2047        let unstamped = expiring_order(&db, &acct, &["old.example.com"], None).await;
2048        expiring_order(&db, &acct, &["new.example.com"], Some(now_secs())).await;
2049        // Never issued: no certificate, so nothing to parse.
2050        Order::create(
2051            "default",
2052            &acct,
2053            vec![Identifier::dns("pending.example.com")],
2054            now_secs() + 3600,
2055            None,
2056            None,
2057            &db,
2058        )
2059        .await
2060        .unwrap();
2061
2062        let rows = Order::find_unstamped("default", 10, &db).await.unwrap();
2063        assert_eq!(rows.len(), 1);
2064        assert_eq!(rows[0].0, unstamped.id);
2065
2066        // And once stamped it stops being returned, which is what stops the
2067        // sweep re-parsing the same row for ever.
2068        Order::set_cert_not_after(&unstamped.id, -1, &db)
2069            .await
2070            .unwrap();
2071        assert!(
2072            Order::find_unstamped("default", 10, &db)
2073                .await
2074                .unwrap()
2075                .is_empty()
2076        );
2077    }
2078}