Skip to main content

acme_proxy_store/
order.rs

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