testsvm_assertions/
lib.rs

1//! # Transaction Assertions
2//!
3//! Assertion helpers for testing transaction results in TestSVM.
4//!
5//! This module provides traits and types for asserting expected transaction outcomes,
6//! including methods for verifying that transactions succeed or fail with specific errors.
7//! These assertions are particularly useful in test environments where you need to
8//! verify that your program behaves correctly under various conditions.
9//!
10//! ## Features
11//!
12//! - **Success/Failure Assertions**: Verify transactions succeed or fail as expected
13//! - **Error Matching**: Check for specific error types including Anchor errors
14//! - **Type-safe API**: Compile-time guarantees for assertion chains
15
16use anyhow::*;
17use litesvm::types::TransactionMetadata;
18use solana_sdk::instruction::InstructionError;
19use testsvm_core::prelude::*;
20
21/// Provides assertion methods for failed transactions.
22///
23/// This struct wraps a transaction error and provides helper methods
24/// for asserting specific error conditions in tests.
25pub struct TXErrorAssertions {
26    /// Underlying transaction error.
27    pub(crate) error: TXError,
28}
29
30impl TXErrorAssertions {
31    /// Asserts that the transaction failed with a specific Anchor error.
32    ///
33    /// This uses string matching to find the error in the transaction logs, looking for
34    /// the last program log containing the string "AnchorError" and matching the error name.
35    pub fn with_anchor_error(&self, error_name: &str) -> Result<()> {
36        match self.error.metadata.err.clone() {
37            solana_sdk::transaction::TransactionError::InstructionError(
38                _,
39                InstructionError::Custom(_error_code),
40            ) => {
41                let maybe_error_message = self
42                    .error
43                    .metadata
44                    .meta
45                    .logs
46                    .iter()
47                    .rev()
48                    .find(|line| line.contains("AnchorError"));
49                if let Some(error_message) = maybe_error_message {
50                    if error_message.contains(&format!("{error_name}. Error Number:")) {
51                        Ok(())
52                    } else {
53                        Err(anyhow!(
54                            "Expected Anchor error '{}', got '{}'",
55                            error_name,
56                            error_message
57                        ))
58                    }
59                } else {
60                    Err(anyhow!(
61                        "Expected Anchor error '{}', but nothing was found in the logs",
62                        error_name
63                    ))
64                }
65            }
66            _ => Err(anyhow!(
67                "Expected error containing '{}', but got '{}'",
68                error_name,
69                self.error.metadata.err.to_string()
70            )),
71        }
72    }
73
74    /// Asserts that the transaction failed with a specific error message.
75    ///
76    /// This method checks the transaction logs for an error message containing
77    /// the specified error name and error code.
78    pub fn with_error(&self, error_name: &str) -> Result<()> {
79        match self.error.metadata.err.clone() {
80            solana_sdk::transaction::TransactionError::InstructionError(
81                _,
82                InstructionError::Custom(error_code),
83            ) => {
84                if self
85                    .error
86                    .metadata
87                    .meta
88                    .pretty_logs()
89                    .contains(format!("{error_name}. Error Number: {error_code}").as_str())
90                {
91                    Ok(())
92                } else {
93                    Err(anyhow!("Expected error '{}'", error_name))
94                }
95            }
96            _ => Err(anyhow!(
97                "Expected error containing '{}', but got '{}'",
98                error_name,
99                self.error.metadata.err.to_string()
100            )),
101        }
102    }
103
104    /// Asserts that the transaction failed with a specific custom error code.
105    ///
106    /// This is useful for checking SPL Token errors and other program-specific error codes.
107    pub fn with_custom_error(&self, error_code: u32) -> Result<()> {
108        match self.error.metadata.err.clone() {
109            solana_sdk::transaction::TransactionError::InstructionError(
110                _,
111                InstructionError::Custom(code),
112            ) => {
113                if code == error_code {
114                    Ok(())
115                } else {
116                    Err(anyhow!(
117                        "Expected custom error code {}, got {}",
118                        error_code,
119                        code
120                    ))
121                }
122            }
123            _ => Err(anyhow!(
124                "Expected custom error code {}, but got '{}'",
125                error_code,
126                self.error.metadata.err.to_string()
127            )),
128        }
129    }
130
131    /// Returns the underlying transaction error for custom assertions.
132    pub fn error(&self) -> &TXError {
133        &self.error
134    }
135}
136
137/// Assertions for successful transactions.
138pub struct TXSuccessAssertions {
139    /// The successful transaction metadata
140    pub metadata: TransactionMetadata,
141}
142
143impl TXSuccessAssertions {
144    /// Returns the compute units consumed by the transaction.
145    pub fn compute_units(&self) -> u64 {
146        self.metadata.compute_units_consumed
147    }
148
149    /// Returns the transaction logs.
150    pub fn logs(&self) -> &Vec<String> {
151        &self.metadata.logs
152    }
153}
154
155/// Extension trait for transaction results providing assertion methods.
156///
157/// This trait adds convenient assertion methods to `TXResult` for testing
158/// whether transactions succeed or fail as expected.
159pub trait TXResultAssertions {
160    /// Asserts that the transaction fails, converting a successful transaction to an error.
161    ///
162    /// This method is used in tests to verify that a transaction is expected to fail.
163    /// It returns a `TXErrorAssertions` struct that provides additional assertion methods
164    /// for checking specific error conditions.
165    ///
166    /// # Returns
167    ///
168    /// Returns `Ok(TXErrorAssertions)` if the transaction failed as expected, or an error
169    /// if the transaction unexpectedly succeeded.
170    ///
171    /// # Example
172    ///
173    /// ```rust
174    /// # use testsvm_core::prelude::*;
175    /// # use testsvm_spl::prelude::*;
176    /// # use testsvm_assertions::*;
177    /// # use solana_sdk::signature::Signer;
178    /// # use anyhow::Result;
179    /// # fn main() -> Result<()> {
180    /// # let mut env = TestSVM::init()?;
181    /// # let owner = env.new_wallet("owner")?;
182    /// # let unauthorized_user = env.new_wallet("unauthorized")?;
183    /// #
184    /// # // Create a mint owned by 'owner'
185    /// # let mint = env.create_mint("test_mint", 6, &owner.pubkey())?;
186    /// #
187    /// # // Create token accounts
188    /// # let (owner_ata_ix, owner_ata) = env.create_ata_ix("owner_ata", &owner.pubkey(), &mint.key)?;
189    /// # let (user_ata_ix, user_ata) = env.create_ata_ix("user_ata", &unauthorized_user.pubkey(), &mint.key)?;
190    /// # env.execute_ixs(&[owner_ata_ix, user_ata_ix])?;
191    /// #
192    /// // Try to mint tokens from unauthorized user (should fail)
193    /// let mint_ix = anchor_spl::token::spl_token::instruction::mint_to(
194    ///     &anchor_spl::token::ID,
195    ///     &mint.key,
196    ///     &user_ata.key,
197    ///     &unauthorized_user.pubkey(), // Wrong authority!
198    ///     &[],
199    ///     1_000_000,
200    /// )?;
201    ///
202    /// // Test that unauthorized minting fails
203    /// let result = env.execute_ixs_with_signers(&[mint_ix], &[&unauthorized_user]);
204    /// result
205    ///     .fails()?  // Assert the transaction fails
206    ///     .with_custom_error(4)?;  // SPL Token error: OwnerMismatch
207    /// # Ok(())
208    /// # }
209    /// ```
210    fn fails(self) -> Result<TXErrorAssertions>;
211
212    /// Asserts that the transaction succeeds, converting a failed transaction to an error.
213    ///
214    /// This method is used in tests to verify that a transaction is expected to succeed.
215    /// It returns a `TXSuccessAssertions` struct that contains the successful transaction
216    /// metadata for further validation if needed.
217    ///
218    /// # Returns
219    ///
220    /// Returns `Ok(TXSuccessAssertions)` if the transaction succeeded as expected, or an error
221    /// if the transaction unexpectedly failed.
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// # use testsvm_core::prelude::*;
227    /// # use testsvm_spl::prelude::*;
228    /// # use testsvm_assertions::*;
229    /// # use solana_sdk::signature::Signer;
230    /// # use anyhow::Result;
231    /// # fn main() -> Result<()> {
232    /// # let mut env = TestSVM::init()?;
233    /// # let owner = env.new_wallet("owner")?;
234    /// #
235    /// # // Create a mint owned by 'owner'
236    /// # let mint = env.create_mint("test_mint", 6, &owner.pubkey())?;
237    /// #
238    /// # // Create token account
239    /// # let (owner_ata_ix, owner_ata) = env.create_ata_ix("owner_ata", &owner.pubkey(), &mint.key)?;
240    /// # env.execute_ixs(&[owner_ata_ix])?;
241    /// #
242    /// // Mint tokens from the authorized owner (should succeed)
243    /// let mint_ix = anchor_spl::token::spl_token::instruction::mint_to(
244    ///     &anchor_spl::token::ID,
245    ///     &mint.key,
246    ///     &owner_ata.key,
247    ///     &owner.pubkey(), // Correct authority
248    ///     &[],
249    ///     1_000_000,
250    /// )?;
251    ///
252    /// // Test that authorized minting succeeds
253    /// let result = env.execute_ixs_with_signers(&[mint_ix], &[&owner]);
254    /// let assertions = result.succeeds()?;
255    ///
256    /// // Can access transaction metadata
257    /// println!("Used {} compute units", assertions.compute_units());
258    /// # Ok(())
259    /// # }
260    /// ```
261    fn succeeds(self) -> Result<TXSuccessAssertions>;
262}
263
264impl TXResultAssertions for TXResult {
265    fn fails(self) -> Result<TXErrorAssertions> {
266        let err = self
267            .err()
268            .ok_or(anyhow::anyhow!("Unexpected successful transaction"))?;
269        Ok(TXErrorAssertions { error: *err })
270    }
271
272    fn succeeds(self) -> Result<TXSuccessAssertions> {
273        let metadata = self?;
274        Ok(TXSuccessAssertions { metadata })
275    }
276}