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