1use std::str::FromStr;
28use std::time::Duration;
29
30use sqlx::migrate::Migrator;
31use sqlx::postgres::{PgPoolOptions, Postgres};
32use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
33use sqlx::{Error, Pool, Sqlite, SqlitePool, migrate::MigrateDatabase};
34use tracing::{error, info};
35
36use crate::sql::{Dialect, Exec};
37
38static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
40
41static PG_MIGRATOR: Migrator = sqlx::migrate!("./migrations-postgres");
46
47pub enum Database {
53 Sqlite(Pool<Sqlite>),
54 Postgres(Pool<Postgres>),
55}
56
57pub enum Tx {
66 Sqlite(sqlx::Transaction<'static, Sqlite>),
67 Postgres(sqlx::Transaction<'static, Postgres>),
68}
69
70impl Tx {
71 pub async fn commit(self) -> Result<(), Error> {
73 match self {
74 Tx::Sqlite(tx) => tx.commit().await,
75 Tx::Postgres(tx) => tx.commit().await,
76 }
77 }
78
79 pub fn conn(&mut self) -> Exec<'_> {
85 match self {
86 Tx::Sqlite(tx) => Exec::SqliteConn(tx),
87 Tx::Postgres(tx) => Exec::PgConn(tx),
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct PoolStats {
95 pub size: u32,
97 pub idle: usize,
99}
100
101impl Database {
102 #[must_use]
104 pub fn dialect(&self) -> Dialect {
105 match self {
106 Database::Sqlite(_) => Dialect::Sqlite,
107 Database::Postgres(_) => Dialect::Postgres,
108 }
109 }
110
111 #[must_use]
113 pub fn exec(&self) -> Exec<'_> {
114 match self {
115 Database::Sqlite(pool) => Exec::SqlitePool(pool),
116 Database::Postgres(pool) => Exec::PgPool(pool),
117 }
118 }
119
120 pub async fn transaction(&self) -> Result<Tx, Error> {
122 Ok(match self {
123 Database::Sqlite(pool) => Tx::Sqlite(pool.begin().await?),
124 Database::Postgres(pool) => Tx::Postgres(pool.begin().await?),
125 })
126 }
127
128 pub async fn write_transaction(&self) -> Result<Tx, Error> {
140 Ok(match self {
141 Database::Sqlite(pool) => Tx::Sqlite(pool.begin_with("BEGIN IMMEDIATE").await?),
142 Database::Postgres(pool) => Tx::Postgres(pool.begin().await?),
148 })
149 }
150
151 #[must_use]
153 pub fn pool_stats(&self) -> PoolStats {
154 match self {
155 Database::Sqlite(pool) => PoolStats {
156 size: pool.size(),
157 idle: pool.num_idle(),
158 },
159 Database::Postgres(pool) => PoolStats {
160 size: pool.size(),
161 idle: pool.num_idle(),
162 },
163 }
164 }
165
166 #[doc(hidden)]
175 #[must_use]
176 pub fn raw_pool(&self) -> &Pool<Sqlite> {
177 match self {
178 Database::Sqlite(pool) => pool,
179 Database::Postgres(_) => {
180 panic!("raw_pool() is a SQLite test fixture; this database is PostgreSQL")
181 }
182 }
183 }
184
185 pub async fn close(&self) {
190 match self {
191 Database::Sqlite(pool) => pool.close().await,
192 Database::Postgres(pool) => pool.close().await,
193 }
194 }
195
196 pub async fn open(url: &str) -> Result<Database, Error> {
218 match scheme_of(url) {
219 Some("sqlite") => Self::open_sqlite(url).await,
220 Some("postgres" | "postgresql") => Self::open_postgres(url).await,
221 _ => Err(Error::Configuration(
222 format!(
223 "unsupported database URL scheme in `{}`: expected one of \
224 sqlite://, postgres:// or postgresql://",
225 acme_proxy_core::logfields::redact_url(url)
226 )
227 .into(),
228 )),
229 }
230 }
231
232 async fn open_sqlite(url: &str) -> Result<Database, Error> {
233 if !Sqlite::database_exists(url).await.unwrap_or(false) {
234 info!(event = "db_creation_started", outcome = "progress", database_url = %url);
235 Sqlite::create_database(url).await?;
236 info!(event = "db_creation_completed", outcome = "success", database_url = %url);
237 }
238
239 let options = SqliteConnectOptions::from_str(url)?
240 .foreign_keys(true)
244 .journal_mode(SqliteJournalMode::Wal)
248 .busy_timeout(Duration::from_secs(5));
249
250 Ok(Database::Sqlite(SqlitePool::connect_with(options).await?))
251 }
252
253 async fn open_postgres(url: &str) -> Result<Database, Error> {
254 let pool = PgPoolOptions::new()
259 .max_connections(10)
260 .acquire_timeout(Duration::from_secs(5))
261 .connect(url)
262 .await?;
263
264 Ok(Database::Postgres(pool))
265 }
266
267 pub async fn connect_and_migrate(url: &str) -> Result<Database, Error> {
273 let database = Self::open(url).await?;
274 database.migrate().await?;
275 Ok(database)
276 }
277
278 pub async fn migrate(&self) -> Result<(), Error> {
283 match self {
284 Database::Sqlite(pool) => run_migrations(&MIGRATOR, pool).await,
285 Database::Postgres(pool) => run_migrations(&PG_MIGRATOR, pool).await,
286 }
287 }
288
289 fn migrator(&self) -> &'static Migrator {
291 match self {
292 Database::Sqlite(_) => &MIGRATOR,
293 Database::Postgres(_) => &PG_MIGRATOR,
294 }
295 }
296
297 pub async fn pending_migrations(&self) -> Result<Vec<i64>, Error> {
306 let applied: std::collections::HashSet<i64> = if self.migrations_table_exists().await? {
307 crate::sql::query("SELECT version FROM _sqlx_migrations;")
308 .fetch_all(self)
309 .await?
310 .iter()
311 .map(|row| row.try_get::<i64>(0usize))
312 .collect::<Result<_, _>>()?
313 } else {
314 std::collections::HashSet::new()
315 };
316
317 Ok(self
318 .migrator()
319 .iter()
320 .filter(|migration| !migration.migration_type.is_down_migration())
321 .map(|migration| migration.version)
322 .filter(|version| !applied.contains(version))
323 .collect())
324 }
325
326 pub async fn connect_in_memory() -> Result<Database, Error> {
330 let pool = SqlitePoolOptions::new()
331 .max_connections(1)
332 .connect_with(SqliteConnectOptions::from_str("sqlite::memory:")?.foreign_keys(true))
333 .await?;
334
335 run_migrations(&MIGRATOR, &pool).await?;
336
337 Ok(Database::Sqlite(pool))
338 }
339
340 #[cfg(any(test, feature = "test-util"))]
360 pub async fn connect_for_test() -> Result<Database, Error> {
361 match crate::testutil::postgres_database().await {
362 Some(database) => Ok(database),
363 None => Self::connect_in_memory().await,
364 }
365 }
366}
367
368impl Database {
376 async fn migrations_table_exists(&self) -> Result<bool, Error> {
377 let sql = match self {
378 Database::Sqlite(_) => {
379 "SELECT COUNT(*) FROM sqlite_master \
380 WHERE type = 'table' AND name = '_sqlx_migrations';"
381 }
382 Database::Postgres(_) => {
389 "SELECT CASE WHEN to_regclass('_sqlx_migrations') IS NULL \
390 THEN 0 ELSE 1 END::bigint;"
391 }
392 };
393 let count: i64 = crate::sql::query(sql)
394 .fetch_one(self)
395 .await?
396 .try_get(0usize)?;
397 Ok(count > 0)
398 }
399}
400
401fn scheme_of(url: &str) -> Option<&str> {
403 let scheme = url.split("://").next()?;
404 (scheme != url).then_some(scheme)
405}
406
407async fn run_migrations<DB>(migrator: &Migrator, pool: &Pool<DB>) -> Result<(), Error>
408where
409 DB: sqlx::Database,
410 DB::Connection: sqlx::migrate::Migrate,
411{
412 migrator.run(pool).await.map_err(|error| {
413 error!(event = "db_migration_failed", outcome = "failure", error = %error);
416 Error::Migrate(Box::new(error))
417 })?;
418 info!(event = "db_migration_completed", outcome = "success");
419 Ok(())
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 use acme_proxy_core::random::random_token;
427
428 #[tokio::test]
429 async fn connect_creates_file_and_runs_migrations() {
430 let file =
433 std::env::temp_dir().join(format!("acme-proxy-test-{}.db", uuid::Uuid::now_v7()));
434 let url = format!("sqlite://{}", file.display());
435
436 let database = Database::connect_and_migrate(&url).await.unwrap();
437
438 let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
440 .fetch_one(database.raw_pool())
441 .await
442 .unwrap();
443 assert_eq!(count, 0);
444
445 let journal: String = sqlx::query_scalar("PRAGMA journal_mode;")
448 .fetch_one(database.raw_pool())
449 .await
450 .unwrap();
451 assert_eq!(journal.to_lowercase(), "wal");
452 let foreign_keys: i64 = sqlx::query_scalar("PRAGMA foreign_keys;")
453 .fetch_one(database.raw_pool())
454 .await
455 .unwrap();
456 assert_eq!(foreign_keys, 1);
457
458 database.close().await;
459 for suffix in ["", "-wal", "-shm"] {
461 let _ = std::fs::remove_file(format!("{}{suffix}", file.display()));
462 }
463 }
464
465 #[tokio::test]
468 async fn a_transaction_commits_or_rolls_back_through_the_wrapper() {
469 let database = Database::connect_in_memory().await.unwrap();
470
471 let mut tx = database.transaction().await.unwrap();
472 crate::sql::query("INSERT INTO nonces VALUES ('dropped', 0);")
473 .execute(tx.conn())
474 .await
475 .unwrap();
476 drop(tx);
477 assert_eq!(count(&database).await, 0, "a dropped Tx rolls back");
478
479 let mut tx = database.transaction().await.unwrap();
480 crate::sql::query("INSERT INTO nonces VALUES ('kept', 0);")
481 .execute(tx.conn())
482 .await
483 .unwrap();
484 tx.commit().await.unwrap();
485 assert_eq!(count(&database).await, 1, "a committed Tx lands");
486
487 let stats = database.pool_stats();
491 assert_eq!(stats.size, 1);
492 assert!(stats.idle <= 1, "{stats:?}");
493
494 database.close().await;
495 let refused = crate::sql::query("SELECT 1;")
496 .execute(database.raw_pool())
497 .await;
498 assert!(refused.is_err(), "a closed pool refuses work");
499 }
500
501 async fn count(database: &Database) -> i64 {
502 sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
503 .fetch_one(database.raw_pool())
504 .await
505 .unwrap()
506 }
507
508 #[tokio::test]
511 async fn foreign_keys_and_the_nonce_sweep_are_indexed() {
512 let database = Database::connect_in_memory().await.unwrap();
513 let names: Vec<String> =
514 sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'index';")
515 .fetch_all(database.raw_pool())
516 .await
517 .unwrap();
518
519 for expected in [
520 "idx_orders_account_id",
521 "idx_authorizations_order",
522 "idx_challenges_authz",
523 "idx_nonces_created_at",
524 "idx_orders_cert_serial",
525 "idx_orders_replaces_claim",
526 "idx_accounts_eab_kid",
529 ] {
530 assert!(
531 names.iter().any(|name| name == expected),
532 "missing index {expected}; have {names:?}"
533 );
534 }
535 }
536
537 #[tokio::test]
547 async fn declared_token_widths_match_random_token() {
548 let database = Database::connect_in_memory().await.unwrap();
549 let expected = format!("VARCHAR({})", random_token().len());
550
551 for (table, column) in [("nonces", "value"), ("challenges", "token")] {
552 assert_eq!(
553 declared_type(&database, table, column).await,
554 expected,
555 "{table}.{column} declares a width the value no longer has"
556 );
557 }
558 }
559
560 #[tokio::test]
564 async fn declared_issuer_widths_match_the_issuer_id() {
565 let database = Database::connect_in_memory().await.unwrap();
566 let expected = format!(
567 "VARCHAR({})",
568 acme_proxy_core::cert::issuer_id(b"any key").len()
569 );
570
571 for (table, column) in [("revocations", "issuer"), ("crls", "issuer")] {
572 assert_eq!(
573 declared_type(&database, table, column).await,
574 expected,
575 "{table}.{column} declares a width the issuer id does not have"
576 );
577 }
578 }
579
580 #[tokio::test]
595 async fn every_id_column_is_declared_a_blob() {
596 let database = Database::connect_in_memory().await.unwrap();
597
598 let minted = crate::id::mint();
599 assert_eq!(
600 minted.get_version_num(),
601 7,
602 "ids are UUID v7 (RFC 9562 §5.7)"
603 );
604 assert_eq!(minted.as_bytes().len(), 16, "which is what a column holds");
605
606 for (table, column) in [
607 ("accounts", "id"),
608 ("accounts", "eab_kid"),
609 ("orders", "id"),
610 ("orders", "account_id"),
611 ("authorizations", "id"),
612 ("authorizations", "order_id"),
613 ("challenges", "id"),
614 ("challenges", "authz_id"),
615 ("eab_keys", "kid"),
616 ("upstream_orders", "order_id"),
617 ("admin_users", "id"),
618 ("admin_sessions", "user_id"),
619 ("admin_recovery_codes", "id"),
620 ("admin_recovery_codes", "user_id"),
621 ("jobs", "id"),
622 ] {
623 assert_eq!(
624 declared_type(&database, table, column).await,
625 "BLOB",
626 "{table}.{column} holds a row id"
627 );
628 }
629
630 for column in ["account_id", "order_id"] {
635 assert_eq!(
636 declared_type(&database, "audit_log", column).await,
637 "VARCHAR(36)",
638 "audit_log.{column} is deliberately still text"
639 );
640 }
641 }
642
643 const BLOB_IDS: i64 = 20_260_827_120_000;
647
648 #[tokio::test]
666 async fn the_blob_migration_preserves_every_row() {
667 let pool = SqlitePoolOptions::new()
668 .max_connections(1)
669 .connect_with(
670 SqliteConnectOptions::from_str("sqlite::memory:")
671 .unwrap()
672 .foreign_keys(true),
673 )
674 .await
675 .unwrap();
676
677 let mut converted = None;
678 for migration in MIGRATOR.iter() {
679 if migration.version == BLOB_IDS {
680 converted = Some(migration);
681 break;
682 }
683 sqlx::raw_sql(migration.sql.clone())
684 .execute(&pool)
685 .await
686 .unwrap();
687 }
688 let converted = converted.expect("the id migration is in the embedded set");
689
690 sqlx::raw_sql(SEED_V4_ROWS).execute(&pool).await.unwrap();
691 sqlx::raw_sql(converted.sql.clone())
692 .execute(&pool)
693 .await
694 .unwrap();
695
696 let account: (Vec<u8>, Option<Vec<u8>>) =
700 sqlx::query_as("SELECT id, eab_kid FROM accounts;")
701 .fetch_one(&pool)
702 .await
703 .unwrap();
704 assert_eq!(
705 uuid::Uuid::from_slice(&account.0).unwrap().to_string(),
706 "11111111-1111-4111-8111-111111111111"
707 );
708 assert_eq!(
709 uuid::Uuid::from_slice(&account.1.expect("eab_kid survived"))
710 .unwrap()
711 .to_string(),
712 "99999999-9999-4999-8999-999999999999"
713 );
714
715 for table in [
716 "orders",
717 "authorizations",
718 "challenges",
719 "upstream_orders",
720 "eab_keys",
721 "admin_users",
722 "admin_sessions",
723 "admin_recovery_codes",
724 "jobs",
725 "audit_log",
726 ] {
727 let rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
731 "SELECT COUNT(*) FROM {table};"
732 )))
733 .fetch_one(&pool)
734 .await
735 .unwrap();
736 assert_eq!(rows, 1, "{table} lost its row");
737 }
738
739 let joined: i64 = sqlx::query_scalar(
743 "SELECT COUNT(*) FROM challenges c \
744 JOIN authorizations a ON a.id = c.authz_id \
745 JOIN orders o ON o.id = a.order_id \
746 JOIN accounts acct ON acct.id = o.account_id;",
747 )
748 .fetch_one(&pool)
749 .await
750 .unwrap();
751 assert_eq!(joined, 1, "the account → challenge chain no longer joins");
752
753 let violations: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM pragma_foreign_key_check;")
754 .fetch_one(&pool)
755 .await
756 .unwrap();
757 assert_eq!(violations, 0);
758
759 let replaces: String = sqlx::query_scalar("SELECT replaces FROM orders;")
761 .fetch_one(&pool)
762 .await
763 .unwrap();
764 assert_eq!(replaces, "aaa.bbb", "an ARI certID is not one of our ids");
765 let audited: String = sqlx::query_scalar("SELECT account_id FROM audit_log;")
766 .fetch_one(&pool)
767 .await
768 .unwrap();
769 assert_eq!(
770 audited, "11111111-1111-4111-8111-111111111111",
771 "audit_log names a row that may be gone, and stays text"
772 );
773
774 sqlx::raw_sql("DELETE FROM accounts;")
777 .execute(&pool)
778 .await
779 .unwrap();
780 for table in ["orders", "authorizations", "challenges"] {
781 let rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
785 "SELECT COUNT(*) FROM {table};"
786 )))
787 .fetch_one(&pool)
788 .await
789 .unwrap();
790 assert_eq!(rows, 0, "deleting the account did not cascade into {table}");
791 }
792 }
793
794 const AUDIT_LOG_ADMIN_ACTIONS: i64 = 20_260_909_120_000;
795
796 #[tokio::test]
804 async fn the_audit_log_rebuild_keeps_every_row_and_relaxes_the_event_check() {
805 let pool = SqlitePoolOptions::new()
806 .max_connections(1)
807 .connect_with(
808 SqliteConnectOptions::from_str("sqlite::memory:")
809 .unwrap()
810 .foreign_keys(true),
811 )
812 .await
813 .unwrap();
814
815 let mut converted = None;
816 for migration in MIGRATOR.iter() {
817 if migration.version == AUDIT_LOG_ADMIN_ACTIONS {
818 converted = Some(migration);
819 break;
820 }
821 sqlx::raw_sql(migration.sql.clone())
822 .execute(&pool)
823 .await
824 .unwrap();
825 }
826 let converted = converted.expect("the audit-log rebuild is in the embedded set");
827
828 sqlx::raw_sql(
829 "INSERT INTO audit_log \
830 (id, created_at, event, outcome, profile, actor_kind, actor_id, account_id, \
831 order_id, cert_serial, identifiers, client_ip, client_ptr, user_agent, \
832 request_id, reason, detail) VALUES \
833 (41812, 1700, 'certificate_revoked', 'success', 'le', 'admin', 'root', \
834 'acct-1', 'order-1', '0a0b', '[\"a.example\"]', '203.0.113.7', 'host.example', \
835 'certbot', 'req-9', '1', 'by operator');",
836 )
837 .execute(&pool)
838 .await
839 .unwrap();
840
841 sqlx::raw_sql(converted.sql.clone())
842 .execute(&pool)
843 .await
844 .unwrap();
845
846 let row = crate::sql::query(
847 "SELECT id, event, actor_id, account_id, order_id, cert_serial, identifiers, \
848 client_ip, user_agent, reason, detail FROM audit_log;",
849 )
850 .fetch_one(&pool)
851 .await
852 .unwrap();
853 assert_eq!(
854 row.try_get::<i64>("id").unwrap(),
855 41812,
856 "the id has to survive, `audit show <id>` uses it"
857 );
858 assert_eq!(
859 row.try_get::<String>("event").unwrap(),
860 "certificate_revoked"
861 );
862 assert_eq!(
863 row.try_get::<Option<String>>("actor_id")
864 .unwrap()
865 .as_deref(),
866 Some("root")
867 );
868 assert_eq!(
869 row.try_get::<Option<String>>("account_id")
870 .unwrap()
871 .as_deref(),
872 Some("acct-1")
873 );
874 assert_eq!(
875 row.try_get::<Option<String>>("order_id")
876 .unwrap()
877 .as_deref(),
878 Some("order-1")
879 );
880 assert_eq!(
881 row.try_get::<String>("identifiers").unwrap(),
882 "[\"a.example\"]"
883 );
884 assert_eq!(
885 row.try_get::<Option<String>>("cert_serial")
886 .unwrap()
887 .as_deref(),
888 Some("0a0b")
889 );
890 assert_eq!(
891 row.try_get::<Option<String>>("client_ip")
892 .unwrap()
893 .as_deref(),
894 Some("203.0.113.7")
895 );
896 assert_eq!(
897 row.try_get::<Option<String>>("detail").unwrap().as_deref(),
898 Some("by operator")
899 );
900
901 assert_eq!(
904 declared_type_on(&pool, "audit_log", "account_id").await,
905 "VARCHAR(36)"
906 );
907
908 sqlx::raw_sql(
911 "INSERT INTO audit_log (created_at, event, outcome, profile, actor_kind) \
912 VALUES (1701, 'operator_teleported', 'success', '', 'admin');",
913 )
914 .execute(&pool)
915 .await
916 .unwrap();
917 let bad_actor = sqlx::raw_sql(
918 "INSERT INTO audit_log (created_at, event, outcome, profile, actor_kind) \
919 VALUES (1702, 'account_deleted', 'success', '', 'robot');",
920 )
921 .execute(&pool)
922 .await;
923 assert!(
924 bad_actor.is_err(),
925 "the actor_kind CHECK still guards the column"
926 );
927
928 let indexes: Vec<String> =
930 sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'index';")
931 .fetch_all(&pool)
932 .await
933 .unwrap();
934 for expected in [
935 "idx_audit_log_created_at",
936 "idx_audit_log_account_id",
937 "idx_audit_log_cert_serial",
938 ] {
939 assert!(
940 indexes.iter().any(|name| name == expected),
941 "{expected} was not re-created"
942 );
943 }
944 }
945
946 async fn declared_type_on(pool: &sqlx::SqlitePool, table: &str, column: &str) -> String {
948 let columns: Vec<(String, String)> =
949 sqlx::query_as("SELECT name, type FROM pragma_table_info(?);")
950 .bind(table)
951 .fetch_all(pool)
952 .await
953 .unwrap();
954 columns
955 .into_iter()
956 .find(|(name, _)| name == column)
957 .map(|(_, declared)| declared)
958 .unwrap_or_else(|| panic!("no column {table}.{column}"))
959 }
960
961 const SEED_V4_ROWS: &str = "\
964INSERT INTO accounts (id, profile, pubkey, contact, status, created_at, eab_kid) VALUES
965 ('11111111-1111-4111-8111-111111111111', 'default', X'AA', '[]', 'valid', 100,
966 '99999999-9999-4999-8999-999999999999');
967INSERT INTO eab_keys (kid, secret, label, profile, status, created_at) VALUES
968 ('99999999-9999-4999-8999-999999999999', X'CC', 'lab', NULL, 'active', 99);
969INSERT INTO orders (id, profile, account_id, status, identifiers, expires, created_at, replaces)
970VALUES
971 ('33333333-3333-4333-8333-333333333333', 'default',
972 '11111111-1111-4111-8111-111111111111', 'pending', '[]', 200, 102, 'aaa.bbb');
973INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) VALUES
974 ('44444444-4444-4444-8444-444444444444', '33333333-3333-4333-8333-333333333333',
975 '{\"type\":\"dns\",\"value\":\"a.example\"}', 'pending', 200, 103);
976INSERT INTO challenges (id, authz_id, type, token, status, created_at, error) VALUES
977 ('55555555-5555-4555-8555-555555555555', '44444444-4444-4444-8444-444444444444',
978 'http-01', 'tok', 'pending', 104, '{\"e\":1}');
979INSERT INTO upstream_orders (order_id, upstream_order_url, csr_der, status, created_at,
980 updated_at, request_id) VALUES
981 ('33333333-3333-4333-8333-333333333333', 'https://up/o', X'DD', 'processing', 105, 105,
982 'req-abc');
983INSERT INTO admin_users (id, username, password_hash, status, created_at, updated_at) VALUES
984 ('66666666-6666-4666-8666-666666666666', 'root', 'h', 'active', 106, 106);
985INSERT INTO admin_sessions (token_hash, user_id, csrf_token, state, created_at, expires_at,
986 last_seen_at) VALUES
987 ('deadbeef', '66666666-6666-4666-8666-666666666666', 'csrf', 'active', 107, 999, 107);
988INSERT INTO admin_recovery_codes (id, user_id, code_hash, created_at) VALUES
989 ('77777777-7777-4777-8777-777777777777', '66666666-6666-4666-8666-666666666666', 'ch', 108);
990INSERT INTO jobs (id, kind, dedup_key, payload, status, run_at, max_attempts, created_at,
991 updated_at, lease_owner) VALUES
992 ('88888888-8888-4888-8888-888888888888', 'k', 'dk', '{}', 'ready', 109, 5, 109, 109,
993 'runner-1');
994INSERT INTO audit_log (created_at, event, outcome, profile, actor_kind, account_id, order_id)
995VALUES
996 (110, 'certificate_issued', 'success', 'default', 'acme',
997 '11111111-1111-4111-8111-111111111111', '33333333-3333-4333-8333-333333333333');
998";
999
1000 async fn declared_type(database: &Database, table: &str, column: &str) -> String {
1002 let columns: Vec<(String, String)> =
1003 sqlx::query_as("SELECT name, type FROM pragma_table_info(?);")
1004 .bind(table)
1005 .fetch_all(database.raw_pool())
1006 .await
1007 .unwrap();
1008
1009 columns
1010 .into_iter()
1011 .find(|(name, _)| name == column)
1012 .map(|(_, declared)| declared)
1013 .unwrap_or_else(|| panic!("no column {table}.{column}"))
1014 }
1015
1016 #[tokio::test]
1024 async fn one_predecessor_can_only_be_claimed_by_one_live_order() {
1025 let database = Database::connect_in_memory().await.unwrap();
1026 let cert_id = "aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE";
1027
1028 crate::sql::query(
1029 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1030 VALUES ('acct', 'default', X'00', '[]', 'valid', 0);",
1031 )
1032 .execute(database.raw_pool())
1033 .await
1034 .unwrap();
1035
1036 let insert = |id: &'static str, status: &'static str| {
1037 let pool = database.raw_pool().clone();
1038 async move {
1039 crate::sql::query(
1040 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, \
1041 replaces, created_at) VALUES (?, 'default', 'acct', ?, '[]', 0, ?, 0);",
1042 )
1043 .bind(id)
1044 .bind(status)
1045 .bind(cert_id)
1046 .execute(&pool)
1047 .await
1048 }
1049 };
1050
1051 insert("first", "pending").await.unwrap();
1052
1053 let error = insert("second", "pending").await.unwrap_err();
1060 match &error {
1061 sqlx::Error::Database(db) => {
1062 assert!(db.is_unique_violation(), "got {error}");
1063 assert!(
1064 db.message().contains("orders.replaces"),
1065 "the violation must name the column, got {:?}",
1066 db.message()
1067 );
1068 }
1069 other => panic!("expected a database error, got {other}"),
1070 }
1071
1072 insert("third", "invalid").await.unwrap();
1075
1076 crate::sql::query("UPDATE orders SET status = 'invalid' WHERE id = 'first';")
1078 .execute(database.raw_pool())
1079 .await
1080 .unwrap();
1081 insert("fourth", "pending").await.unwrap();
1082 }
1083
1084 #[tokio::test]
1088 async fn status_columns_reject_values_outside_the_state_machine() {
1089 let database = Database::connect_in_memory().await.unwrap();
1090
1091 let result = crate::sql::query(
1092 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1093 VALUES ('a', 'default', X'00', '[]', 'definitely-not-a-status', 0);",
1094 )
1095 .execute(database.raw_pool())
1096 .await;
1097 assert!(result.is_err(), "an unknown account status must be refused");
1098
1099 crate::sql::query(
1101 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1102 VALUES ('a', 'default', X'00', '[]', 'valid', 0);",
1103 )
1104 .execute(database.raw_pool())
1105 .await
1106 .unwrap();
1107 }
1108
1109 #[tokio::test]
1113 async fn deleting_an_account_cascades_to_its_orders() {
1114 let database = Database::connect_in_memory().await.unwrap();
1115
1116 crate::sql::query(
1117 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1118 VALUES ('acct', 'default', X'00', '[]', 'valid', 0);",
1119 )
1120 .execute(database.raw_pool())
1121 .await
1122 .unwrap();
1123 crate::sql::query(
1124 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, created_at) \
1125 VALUES ('ord', 'default', 'acct', 'pending', '[]', 0, 0);",
1126 )
1127 .execute(database.raw_pool())
1128 .await
1129 .unwrap();
1130 crate::sql::query(
1131 "INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) \
1132 VALUES ('az', 'ord', '{}', 'pending', 0, 0);",
1133 )
1134 .execute(database.raw_pool())
1135 .await
1136 .unwrap();
1137 crate::sql::query(
1138 "INSERT INTO challenges (id, authz_id, type, token, status, created_at) \
1139 VALUES ('ch', 'az', 'http-01', 't', 'pending', 0);",
1140 )
1141 .execute(database.raw_pool())
1142 .await
1143 .unwrap();
1144
1145 crate::sql::query("DELETE FROM accounts WHERE id = 'acct';")
1146 .execute(database.raw_pool())
1147 .await
1148 .unwrap();
1149
1150 for (table, query) in [
1151 ("orders", "SELECT COUNT(*) FROM orders;"),
1152 ("authorizations", "SELECT COUNT(*) FROM authorizations;"),
1153 ("challenges", "SELECT COUNT(*) FROM challenges;"),
1154 ] {
1155 let count: i64 = sqlx::query_scalar(query)
1156 .fetch_one(database.raw_pool())
1157 .await
1158 .unwrap();
1159 assert_eq!(count, 0, "{table} should have been cascaded away");
1160 }
1161 }
1162
1163 #[tokio::test]
1165 async fn an_order_cannot_have_duplicate_authorizations_for_one_identifier() {
1166 let database = Database::connect_in_memory().await.unwrap();
1167 crate::sql::query(
1168 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1169 VALUES ('acct', 'default', X'00', '[]', 'valid', 0);",
1170 )
1171 .execute(database.raw_pool())
1172 .await
1173 .unwrap();
1174 crate::sql::query(
1175 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, created_at) \
1176 VALUES ('ord', 'default', 'acct', 'pending', '[]', 0, 0);",
1177 )
1178 .execute(database.raw_pool())
1179 .await
1180 .unwrap();
1181
1182 let insert = |id: &'static str| {
1183 crate::sql::query(
1184 "INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) \
1185 VALUES (?, 'ord', '{\"type\":\"dns\",\"value\":\"example.com\"}', 'pending', 0, 0);",
1186 )
1187 .bind(id)
1188 .execute(database.raw_pool())
1189 };
1190
1191 insert("az1").await.unwrap();
1192 assert!(
1193 insert("az2").await.is_err(),
1194 "a second authorization for the same identifier must be refused"
1195 );
1196 }
1197}