use super::*;
use alloy::primitives::address;
fn store(dir: &tempfile::TempDir) -> anyhow::Result<RedbBuyerPoolStore> {
Ok(RedbBuyerPoolStore::open(&dir.path().join("d"))?)
}
fn state(byte: u8, bytes: u64, amount: u64) -> anyhow::Result<(BuyerPoolState, LaneKey)> {
let owner = Address::repeat_byte(byte);
let pool_id = PoolId::repeat_byte(byte);
let lane = LaneKey {
pool_id,
signer: owner,
provider: address!("00000000000000000000000000000000000000aa"),
};
let mut s = BuyerPoolState::new(
pool_id,
crate::Deployment {
chain_id: 421_614,
payment_pool: Address::repeat_byte(0x9c),
},
owner,
Address::repeat_byte(0xaa),
U256::from(1_000_000u64),
);
s.advance_lane(lane, U256::from(bytes), U256::from(amount))?;
Ok((s, lane))
}
#[test]
fn second_concurrent_open_is_already_open_not_raw_backend() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let data = dir.path().join("d");
let first = RedbBuyerPoolStore::open(&data)?;
let err = RedbBuyerPoolStore::open(&data)
.err()
.ok_or_else(|| anyhow::anyhow!("a second concurrent open must fail"))?;
anyhow::ensure!(
matches!(&err, StoreError::AlreadyOpen { path } if path == &data.join(BUYER_POOLS_DB_FILE)),
"wrong error or path: {err:?}"
);
let msg = err.to_string();
anyhow::ensure!(
msg.contains("another decdn process is using") && msg.contains("--data-dir"),
"message must be user-facing, got: {msg}"
);
drop(first);
RedbBuyerPoolStore::open(&data)?;
Ok(())
}
#[test]
fn a_closed_store_reads_back_read_only() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let data = dir.path().join("d");
let (recorded, _) = state(7, 4096, 9)?;
{
let store = RedbBuyerPoolStore::open(&data)?;
store.record(&recorded)?;
}
let path = data.join(BUYER_POOLS_DB_FILE);
let reader = ReadOnlyBuyerPoolStore::open_file(&path)?;
anyhow::ensure!(reader.path() == path, "the reader must name its file");
let load = reader.load_all()?;
anyhow::ensure!(load.skipped.is_empty(), "no row should be skipped");
anyhow::ensure!(
load.pools == vec![recorded],
"read-only load must match what was written: {:?}",
load.pools
);
Ok(())
}
#[test]
fn read_only_open_reports_a_live_writer_as_already_open() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let data = dir.path().join("d");
let writer = RedbBuyerPoolStore::open(&data)?;
let path = data.join(BUYER_POOLS_DB_FILE);
let err = ReadOnlyBuyerPoolStore::open_file(&path)
.err()
.ok_or_else(|| anyhow::anyhow!("a read-only open must not race a live writer"))?;
anyhow::ensure!(
matches!(&err, StoreError::AlreadyOpen { path: p } if p == &path),
"wrong error or path: {err:?}"
);
drop(writer);
ReadOnlyBuyerPoolStore::open_file(&path)?;
Ok(())
}
#[test]
fn read_only_open_creates_nothing() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("buyer.redb");
anyhow::ensure!(
ReadOnlyBuyerPoolStore::open_file(&path).is_err(),
"an absent store must not open"
);
anyhow::ensure!(!path.exists(), "a read-only open must not create the file");
Ok(())
}
#[test]
fn rejects_zero_length_db_file() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let data = dir.path().join("d");
drop(store(&dir)?); std::fs::write(data.join(BUYER_POOLS_DB_FILE), b"")?;
let err = RedbBuyerPoolStore::open(&data)
.err()
.ok_or_else(|| anyhow::anyhow!("a zero-length db file must be rejected"))?;
anyhow::ensure!(
matches!(&err, StoreError::Corrupt { detail, .. } if detail.contains("empty (length 0)")),
"expected Corrupt(empty), got {err:?}"
);
Ok(())
}
#[test]
fn record_then_reuse_resumes_watermark_across_reopen() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let (s, _lane) = state(1, 5_000, 50)?;
{
let store = store(&dir)?;
store.record(&s)?;
}
let reopened = store(&dir)?;
let got = reopened
.get_by_owner(s.owner)?
.ok_or_else(|| anyhow::anyhow!("row vanished across reopen"))?;
anyhow::ensure!(got == s, "watermark must resume across invocations");
Ok(())
}
#[test]
fn store_delegates_every_op() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let store = store(&dir)?;
let (s, lane) = state(2, 1_000, 10)?;
store.record(&s)?;
anyhow::ensure!(
store.get_by_owner(s.owner)?.as_ref() == Some(&s),
"record/get"
);
anyhow::ensure!(store.load_all()?.pools.len() == 1, "load_all");
anyhow::ensure!(
store.advance_progress(
s.owner,
s.pool_id,
lane,
U256::from(2_000u64),
U256::from(20u64),
)? == AdvanceOutcome::Advanced,
"advance_progress"
);
anyhow::ensure!(
store.add_deposit(s.owner, s.pool_id, U256::from(7u64))?
== DepositOutcome::Added(s.deposit + U256::from(7u64)),
"add_deposit"
);
let row = store
.get_by_owner(s.owner)?
.ok_or_else(|| anyhow::anyhow!("row vanished"))?;
let progress = row
.lane_progress(lane)
.ok_or_else(|| anyhow::anyhow!("lane vanished"))?;
anyhow::ensure!(
progress.last_bytes == U256::from(2_000u64),
"last_bytes = {}",
progress.last_bytes
);
anyhow::ensure!(
progress.last_amount == U256::from(20u64),
"last_amount = {}",
progress.last_amount
);
anyhow::ensure!(
row.deposit == s.deposit + U256::from(7u64),
"deposit = {}",
row.deposit
);
anyhow::ensure!(
!store.forget_if_pool(s.owner, PoolId::repeat_byte(0xee))?,
"forget_if_pool must not delete on a mismatched pool"
);
anyhow::ensure!(store.get_by_owner(s.owner)?.is_some(), "row must survive");
anyhow::ensure!(
store.forget_if_pool(s.owner, s.pool_id)?,
"forget_if_pool must delete on a match"
);
anyhow::ensure!(store.get_by_owner(s.owner)?.is_none(), "row must be gone");
store.record(&s)?;
store.forget(s.owner)?;
anyhow::ensure!(store.load_all()?.pools.is_empty(), "forget");
Ok(())
}