Ver código fonte

Add test for remove_proofs failing on spent proofs

Add a comprehensive test that verifies the existing behavior of the
`remove_proofs` method when attempting to remove proofs in the `Spent` state.

The test `remove_spent_proofs_should_fail` verifies:
- Removing Unspent proofs succeeds
- Removing Pending proofs succeeds
- Removing Spent proofs fails with `AttemptRemoveSpentProof` error
- Proofs remain in database after failed removal attempt

This test ensures the storage layer enforces the constraint that spent proofs
cannot be removed, which is important for maintaining data integrity and audit
trails.
Cesar Rodas 1 mês atrás
pai
commit
341b653362

+ 2 - 1
crates/cdk-common/src/database/mint/test/mod.rs

@@ -302,7 +302,8 @@ macro_rules! mint_db_test {
             get_mint_quote_by_request_in_transaction,
             get_mint_quote_by_request_lookup_id_in_transaction,
             get_blind_signatures_in_transaction,
-            reject_duplicate_payment_ids
+            reject_duplicate_payment_ids,
+            remove_spent_proofs_should_fail
         );
     };
     ($make_db_fn:ident, $($name:ident),+ $(,)?) => {

+ 110 - 0
crates/cdk-common/src/database/mint/test/proofs.rs

@@ -616,3 +616,113 @@ where
     let total: u64 = retrieved_proofs.iter().map(|p| u64::from(p.amount)).sum();
     assert!(total >= 5500); // 100 + 200 + ... + 1000 = 5500
 }
+
+/// Test that removing proofs in Spent state should fail
+///
+/// This test verifies that the storage layer enforces the constraint that proofs
+/// in the `Spent` state cannot be removed via `remove_proofs`. The operation should
+/// fail with an error to prevent accidental deletion of spent proofs.
+pub async fn remove_spent_proofs_should_fail<DB>(db: DB)
+where
+    DB: Database<Error> + KeysDatabase<Err = Error>,
+{
+    use cashu::State;
+
+    let keyset_id = setup_keyset(&db).await;
+    let quote_id = QuoteId::new_uuid();
+
+    let proofs = vec![
+        Proof {
+            amount: Amount::from(100),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+        Proof {
+            amount: Amount::from(200),
+            keyset_id,
+            secret: Secret::generate(),
+            c: SecretKey::generate().public_key(),
+            witness: None,
+            dleq: None,
+        },
+    ];
+
+    let ys: Vec<_> = proofs.iter().map(|p| p.y().unwrap()).collect();
+
+    // Add proofs to database (initial state is Unspent)
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    tx.add_proofs(
+        proofs.clone(),
+        Some(quote_id.clone()),
+        &Operation::new_swap(),
+    )
+    .await
+    .unwrap();
+    tx.commit().await.unwrap();
+
+    // Verify proofs exist and are in Unspent state
+    let states = db.get_proofs_states(&ys).await.unwrap();
+    assert_eq!(states.len(), 2);
+    assert_eq!(states[0], Some(State::Unspent));
+    assert_eq!(states[1], Some(State::Unspent));
+
+    // Removing Unspent proofs should succeed
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    let result = tx.remove_proofs(&[ys[0]], Some(quote_id.clone())).await;
+    assert!(result.is_ok(), "Removing Unspent proof should succeed");
+    tx.rollback().await.unwrap(); // Rollback to keep proofs for next test
+
+    // Transition proofs to Pending state
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    tx.update_proofs_states(&ys, State::Pending).await.unwrap();
+    tx.commit().await.unwrap();
+
+    // Removing Pending proofs should also succeed
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    let result = tx.remove_proofs(&[ys[0]], Some(quote_id.clone())).await;
+    assert!(result.is_ok(), "Removing Pending proof should succeed");
+    tx.rollback().await.unwrap(); // Rollback to keep proofs for next test
+
+    // Now transition proofs to Spent state
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    tx.update_proofs_states(&ys, State::Spent).await.unwrap();
+    tx.commit().await.unwrap();
+
+    // Verify proofs are now in Spent state
+    let states = db.get_proofs_states(&ys).await.unwrap();
+    assert_eq!(states[0], Some(State::Spent));
+    assert_eq!(states[1], Some(State::Spent));
+
+    // Attempt to remove Spent proofs - this should FAIL
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    let result = tx.remove_proofs(&ys, Some(quote_id.clone())).await;
+    assert!(
+        result.is_err(),
+        "Removing proofs in Spent state should fail"
+    );
+
+    // Verify the error is the expected type
+    assert!(
+        matches!(result.unwrap_err(), Error::AttemptRemoveSpentProof),
+        "Error should be AttemptRemoveSpentProof"
+    );
+
+    // Rollback the failed transaction to release locks
+    tx.rollback().await.unwrap();
+
+    // Verify proofs still exist after failed removal attempt
+    let states = db.get_proofs_states(&ys).await.unwrap();
+    assert_eq!(
+        states[0],
+        Some(State::Spent),
+        "First proof should still exist"
+    );
+    assert_eq!(
+        states[1],
+        Some(State::Spent),
+        "Second proof should still exist"
+    );
+}