use std::sync::Arc;
use acme_proxy_core::audit::ClientContext;
use acme_proxy_store::db::Database;
use acme_proxy_store::order::{Order, OrderQuery};
use acme_proxy_store::testutil;
macro_rules! each_backend {
(|$db:ident| $body:expr) => {{
{
let $db = Arc::new(
Database::connect_in_memory()
.await
.expect("an in-memory SQLite always opens"),
);
$body;
}
if let Some(database) = testutil::postgres_database().await {
let $db = Arc::new(database);
$body;
}
}};
}
async fn order_with(database: &Arc<Database>, names: &[&str]) -> Order {
let account_id = testutil::account_id(database).await;
Order::create(
"default",
account_id,
testutil::dns_identifiers(names),
EXPIRES,
None,
None,
database,
)
.await
.expect("an order should be storable")
}
const EXPIRES: i64 = 4_102_444_800;
#[tokio::test]
async fn the_identifier_search_matches_exactly_on_both_backends() {
each_backend!(|db| {
order_with(&db, &["example.com"]).await;
order_with(&db, &["evil-example.com"]).await;
let found = |value: &str| {
let db = db.clone();
let value = value.to_string();
async move {
let query = OrderQuery {
identifier: Some(value),
..Default::default()
};
Order::search(&query, &db).await.expect("search works").1
}
};
assert_eq!(found("example.com").await, 1, "the exact name");
assert_eq!(
found("evil-example.com").await,
1,
"and the other one, on its own"
);
assert_eq!(found("ample.com").await, 0, "a suffix is not a match");
assert_eq!(found("nothing.invalid").await, 0);
});
}
#[tokio::test]
async fn the_substring_search_treats_wildcards_as_literals_on_both_backends() {
each_backend!(|db| {
order_with(&db, &["shop.example.com"]).await;
let found = |value: &str| {
let db = db.clone();
let value = value.to_string();
async move {
let query = OrderQuery {
identifier_contains: Some(value),
..Default::default()
};
Order::search(&query, &db).await.expect("search works").1
}
};
assert_eq!(found("example").await, 1, "a fragment in the middle");
assert_eq!(found("SHOP").await, 1, "folded to lower case");
assert_eq!(
found("%").await,
0,
"a percent is a character, not a wildcard"
);
assert_eq!(found("_").await, 0, "and so is an underscore");
assert_eq!(found("zzz").await, 0);
});
}
#[tokio::test]
async fn a_nonce_is_spent_exactly_once_on_both_backends() {
use acme_proxy_store::nonce::Nonce;
use std::time::Duration;
each_backend!(|db| {
let nonce = Nonce::new();
let value = nonce.value.clone();
nonce.save(&db).await.expect("a nonce should be storable");
let ttl = Duration::from_secs(300);
let mut wins = 0;
for _ in 0..5 {
if Nonce::verify(&value, &db, ttl).await.expect("verify works") {
wins += 1;
}
}
assert_eq!(wins, 1, "exactly one caller may spend a nonce");
});
}
#[tokio::test]
async fn one_predecessor_can_only_be_claimed_once_on_both_backends() {
each_backend!(|db| {
let account_id = testutil::account_id(&db).await;
let cert_id = "some-certID";
let claim = |suffix: &str| {
let db = db.clone();
let name = format!("{suffix}.example");
async move {
let mut order = Order::new(
"default",
account_id,
testutil::dns_identifiers(&[&name]),
EXPIRES,
None,
None,
);
order.replaces = Some(cert_id.to_string());
order.insert(&db).await.map(|()| order)
}
};
claim("first").await.expect("the first claim is taken");
let error = claim("second")
.await
.expect_err("a second live claim on one predecessor is refused");
assert!(
acme_proxy_store::sql::is_unique_violation_on(
&error,
"orders.replaces",
"idx_orders_replaces_claim",
),
"the refusal has to be recognisable as the replaces claim, or the \
handler answers 500 instead of 409 alreadyReplaced: {error}"
);
});
}
#[tokio::test]
async fn an_account_with_every_nullable_column_unset_round_trips_on_both_backends() {
use acme_proxy_store::account::Account;
each_backend!(|db| {
let (created, fresh) = Account::find_or_create(
"default",
&[7u8, 8, 9],
vec![],
&ClientContext::default(),
&db,
)
.await
.expect("an account with no optional column set should store");
assert!(fresh, "the first call creates it");
let read = Account::find_by_id("default", &created.id.to_string(), &db)
.await
.expect("the account should be readable")
.expect("and present");
assert_eq!(read.id, created.id);
assert_eq!(read.pubkey, vec![7u8, 8, 9], "a blob survives as bytes");
assert_eq!(read.eab_kid, None, "an unset uuid reads back as absent");
assert_eq!(read.terms_of_service_agreed, None, "and an unset boolean");
assert_eq!(read.created_ip, None, "and an unset text column");
let order = order_with(&db, &["nulls.example"]).await;
let read = Order::find_by_id(&order.id.to_string(), &db)
.await
.expect("the order should be readable")
.expect("and present");
assert_eq!(
read.not_before, None,
"an unset integer reads back as absent"
);
assert_eq!(read.not_after, None);
assert_eq!(read.certificate, None);
});
}
#[tokio::test]
async fn paging_agrees_with_the_unpaged_total_on_both_backends() {
each_backend!(|db| {
for index in 0..7 {
order_with(&db, &[&format!("page-{index}.example")]).await;
}
let page = |limit: i64, offset: i64| {
let db = db.clone();
async move {
let query = OrderQuery {
limit,
offset,
..Default::default()
};
Order::search(&query, &db).await.expect("search works")
}
};
let (first, total) = page(3, 0).await;
assert_eq!(total, 7, "the total ignores the page");
assert_eq!(first.len(), 3);
let (second, _) = page(3, 3).await;
let (third, _) = page(3, 6).await;
assert_eq!(third.len(), 1, "the last page is the remainder");
let seen: Vec<_> = first
.iter()
.chain(&second)
.chain(&third)
.map(|order| order.id)
.collect();
let mut unique = seen.clone();
unique.sort_unstable();
unique.dedup();
assert_eq!(
unique.len(),
7,
"no row may appear on two pages or on none: {seen:?}"
);
});
}
#[tokio::test]
async fn a_job_identity_is_held_by_one_live_row_on_both_backends() {
use acme_proxy_store::job::{Job, NewJob};
each_backend!(|db| {
let payload = serde_json::json!({});
let spec = |id| NewJob {
id,
kind: "test_kind",
dedup_key: "the-one-key",
payload: &payload,
run_at: 0,
deadline: None,
max_attempts: 3,
};
assert!(
Job::enqueue(spec(acme_proxy_store::id::mint()), &db)
.await
.expect("the first enqueue works"),
"the first job takes the identity"
);
assert!(
!Job::enqueue(spec(acme_proxy_store::id::mint()), &db)
.await
.expect("the second enqueue is not an error"),
"a live job already holds this (kind, dedup_key)"
);
assert_eq!(
Job::count_live("test_kind", &db)
.await
.expect("counting works"),
1
);
});
}
#[tokio::test]
async fn one_key_registering_twice_is_one_account_on_both_backends() {
use acme_proxy_store::account::Account;
each_backend!(|db| {
let key = [9u8, 9, 9];
let (first, created) =
Account::find_or_create("default", &key, vec![], &ClientContext::default(), &db)
.await
.expect("the first registration works");
assert!(created, "the first call creates the account");
let (second, created) =
Account::find_or_create("default", &key, vec![], &ClientContext::default(), &db)
.await
.expect("the second registration finds it");
assert!(!created, "the second call finds the first account");
assert_eq!(first.id, second.id);
let error = acme_proxy_store::sql::query(
"INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
VALUES (?, 'default', ?, '[]', 'valid', 0);",
)
.bind(acme_proxy_store::id::mint())
.bind(&key[..])
.execute(&db)
.await
.expect_err("a second row for one key is refused");
assert!(
acme_proxy_store::account::is_pubkey_conflict(&error),
"the refusal has to be recognisable as the pubkey constraint, or a \
repeat registration answers 500: {error}"
);
});
}
#[tokio::test]
async fn the_declared_widths_are_enforced_on_postgres() {
use acme_proxy_core::random::random_token;
let Some(db) = testutil::postgres_database().await else {
return;
};
let width = |table: &'static str, column: &'static str| {
let db = db.exec();
async move {
acme_proxy_store::sql::query(
"SELECT character_maximum_length::bigint FROM information_schema.columns \
WHERE table_name = ? AND column_name = ?;",
)
.bind(table)
.bind(column)
.fetch_one(db)
.await
.expect("the column should exist")
.try_get::<i64>(0usize)
.expect("a varchar declares a length")
}
};
let token = i64::try_from(random_token().len()).expect("a token length fits");
assert_eq!(width("nonces", "value").await, token);
assert_eq!(width("challenges", "token").await, token);
let issuer = i64::try_from(acme_proxy_core::cert::issuer_id(&[1, 2, 3]).len())
.expect("an issuer id fits");
assert_eq!(width("revocations", "issuer").await, issuer);
assert_eq!(width("crls", "issuer").await, issuer);
assert_eq!(width("audit_log", "account_id").await, 36);
assert_eq!(width("audit_log", "order_id").await, 36);
}
#[tokio::test]
async fn a_database_survives_a_round_trip_through_the_other_backend() {
let Some(postgres) = testutil::postgres_database().await else {
return;
};
let source = Arc::new(
Database::connect_in_memory()
.await
.expect("an in-memory SQLite always opens"),
);
testutil::seed_every_table(&source).await;
let before = testutil::row_counts(&source).await;
assert!(
before.iter().all(|(_, rows)| *rows > 0),
"every table must be seeded or the round trip proves nothing: {before:?}"
);
let out = source
.transfer_to(&postgres)
.await
.expect("the copy into PostgreSQL works");
assert_eq!(out.total(), before.iter().map(|(_, n)| n).sum::<u64>());
assert_eq!(
testutil::row_counts(&postgres).await,
before,
"counts after the copy out"
);
let returned = Database::connect_in_memory()
.await
.expect("a second SQLite");
postgres
.transfer_to(&returned)
.await
.expect("the copy back into SQLite works");
assert_eq!(
testutil::row_counts(&returned).await,
before,
"counts after the copy back"
);
let original = Order::search(&OrderQuery::default(), &source)
.await
.expect("the source lists")
.0;
let copied = Order::search(&OrderQuery::default(), &returned)
.await
.expect("the round-tripped database lists")
.0;
assert_eq!(copied.len(), original.len());
for (was, now) in original.iter().zip(&copied) {
assert_eq!(now.id, was.id, "the id an operator and a URL both carry");
assert_eq!(
now.cert_serial, was.cert_serial,
"revocation finds it by this"
);
assert_eq!(now.cert_pubkey, was.cert_pubkey, "a blob column");
assert_eq!(now.certificate, was.certificate);
assert_eq!(now.identifiers, was.identifiers, "a JSON column");
assert_eq!(now.status, was.status);
assert_eq!(
now.not_after, was.not_after,
"an absent integer stays absent"
);
}
async fn audit_ids(database: &Database) -> Vec<i64> {
acme_proxy_store::sql::query("SELECT id FROM audit_log ORDER BY id;")
.fetch_all(database)
.await
.expect("audit ids should be listable")
.iter()
.map(|row| row.try_get::<i64>(0usize).expect("an id"))
.collect()
}
assert_eq!(audit_ids(&returned).await, audit_ids(&source).await);
}
#[tokio::test]
async fn postgres_is_available_when_it_is_required() {
if std::env::var_os(testutil::REQUIRE_POSTGRES).is_none() {
return;
}
assert!(
testutil::postgres_database().await.is_some(),
"{} is set but no PostgreSQL could be reached through {}",
testutil::REQUIRE_POSTGRES,
testutil::TEST_POSTGRES_URL,
);
}