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, program_error::ProgramError};
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    /// Asserts that the transaction failed with a specific program error.
132    pub fn with_program_error<T: Into<ProgramError>>(&self, err: T) -> Result<()> {
133        let program_error: ProgramError = err.into();
134        match self.error.metadata.err.clone() {
135            solana_sdk::transaction::TransactionError::InstructionError(_, err) => {
136                let result_program_error: ProgramError = err.try_into()?;
137                if result_program_error == program_error {
138                    Ok(())
139                } else {
140                    Err(anyhow!(
141                        "Expected custom program error {}, but got '{}'",
142                        program_error,
143                        result_program_error
144                    ))
145                }
146            }
147            _ => Err(anyhow!(
148                "Expected custom program error {}, but got instruction error '{}'",
149                program_error,
150                self.error.metadata.err.to_string()
151            )),
152        }
153    }
154
155    /// Returns the underlying transaction error for custom assertions.
156    pub fn error(&self) -> &TXError {
157        &self.error
158    }
159}
160
161/// Assertions for successful transactions.
162pub struct TXSuccessAssertions {
163    /// The successful transaction metadata
164    pub metadata: TransactionMetadata,
165}
166
167impl TXSuccessAssertions {
168    /// Returns the compute units consumed by the transaction.
169    pub fn compute_units(&self) -> u64 {
170        self.metadata.compute_units_consumed
171    }
172
173    /// Returns the transaction logs.
174    pub fn logs(&self) -> &Vec<String> {
175        &self.metadata.logs
176    }
177}
178
179/// Extension trait for transaction results providing assertion methods.
180///
181/// This trait adds convenient assertion methods to `TXResult` for testing
182/// whether transactions succeed or fail as expected.
183pub trait TXResultAssertions {
184    /// Asserts that the transaction fails, converting a successful transaction to an error.
185    ///
186    /// This method is used in tests to verify that a transaction is expected to fail.
187    /// It returns a `TXErrorAssertions` struct that provides additional assertion methods
188    /// for checking specific error conditions.
189    ///
190    /// # Returns
191    ///
192    /// Returns `Ok(TXErrorAssertions)` if the transaction failed as expected, or an error
193    /// if the transaction unexpectedly succeeded.
194    ///
195    /// # Example
196    ///
197    /// ```rust
198    /// # use testsvm_core::prelude::*;
199    /// # use testsvm_spl::prelude::*;
200    /// # use testsvm_assertions::*;
201    /// # use solana_sdk::signature::Signer;
202    /// # use anyhow::Result;
203    /// # fn main() -> Result<()> {
204    /// # let mut env = TestSVM::init()?;
205    /// # let owner = env.new_wallet("owner")?;
206    /// # let unauthorized_user = env.new_wallet("unauthorized")?;
207    /// #
208    /// # // Create a mint owned by 'owner'
209    /// # let mint = env.create_mint("test_mint", 6, &owner.pubkey())?;
210    /// #
211    /// # // Create token accounts
212    /// # let (owner_ata_ix, owner_ata) = env.create_ata_ix("owner_ata", &owner.pubkey(), &mint.key)?;
213    /// # let (user_ata_ix, user_ata) = env.create_ata_ix("user_ata", &unauthorized_user.pubkey(), &mint.key)?;
214    /// # env.execute_ixs(&[owner_ata_ix, user_ata_ix])?;
215    /// #
216    /// // Try to mint tokens from unauthorized user (should fail)
217    /// let mint_ix = anchor_spl::token::spl_token::instruction::mint_to(
218    ///     &anchor_spl::token::ID,
219    ///     &mint.key,
220    ///     &user_ata.key,
221    ///     &unauthorized_user.pubkey(), // Wrong authority!
222    ///     &[],
223    ///     1_000_000,
224    /// )?;
225    ///
226    /// // Test that unauthorized minting fails
227    /// let result = env.execute_ixs_with_signers(&[mint_ix], &[&unauthorized_user]);
228    /// result
229    ///     .fails()?  // Assert the transaction fails
230    ///     .with_custom_error(4)?;  // SPL Token error: OwnerMismatch
231    /// # Ok(())
232    /// # }
233    /// ```
234    fn fails(self) -> Result<TXErrorAssertions>;
235
236    /// Asserts that the transaction succeeds, converting a failed transaction to an error.
237    ///
238    /// This method is used in tests to verify that a transaction is expected to succeed.
239    /// It returns a `TXSuccessAssertions` struct that contains the successful transaction
240    /// metadata for further validation if needed.
241    ///
242    /// # Returns
243    ///
244    /// Returns `Ok(TXSuccessAssertions)` if the transaction succeeded as expected, or an error
245    /// if the transaction unexpectedly failed.
246    ///
247    /// # Example
248    ///
249    /// ```rust
250    /// # use testsvm_core::prelude::*;
251    /// # use testsvm_spl::prelude::*;
252    /// # use testsvm_assertions::*;
253    /// # use solana_sdk::signature::Signer;
254    /// # use anyhow::Result;
255    /// # fn main() -> Result<()> {
256    /// # let mut env = TestSVM::init()?;
257    /// # let owner = env.new_wallet("owner")?;
258    /// #
259    /// # // Create a mint owned by 'owner'
260    /// # let mint = env.create_mint("test_mint", 6, &owner.pubkey())?;
261    /// #
262    /// # // Create token account
263    /// # let (owner_ata_ix, owner_ata) = env.create_ata_ix("owner_ata", &owner.pubkey(), &mint.key)?;
264    /// # env.execute_ixs(&[owner_ata_ix])?;
265    /// #
266    /// // Mint tokens from the authorized owner (should succeed)
267    /// let mint_ix = anchor_spl::token::spl_token::instruction::mint_to(
268    ///     &anchor_spl::token::ID,
269    ///     &mint.key,
270    ///     &owner_ata.key,
271    ///     &owner.pubkey(), // Correct authority
272    ///     &[],
273    ///     1_000_000,
274    /// )?;
275    ///
276    /// // Test that authorized minting succeeds
277    /// let result = env.execute_ixs_with_signers(&[mint_ix], &[&owner]);
278    /// let assertions = result.succeeds()?;
279    ///
280    /// // Can access transaction metadata
281    /// println!("Used {} compute units", assertions.compute_units());
282    /// # Ok(())
283    /// # }
284    /// ```
285    fn succeeds(self) -> Result<TXSuccessAssertions>;
286}
287
288impl TXResultAssertions for TXResult {
289    fn fails(self) -> Result<TXErrorAssertions> {
290        let err = self
291            .err()
292            .ok_or(anyhow::anyhow!("Unexpected successful transaction"))?;
293        Ok(TXErrorAssertions { error: *err })
294    }
295
296    fn succeeds(self) -> Result<TXSuccessAssertions> {
297        match self {
298            Result::Ok(metadata) => Ok(TXSuccessAssertions { metadata }),
299            Result::Err(e) => {
300                e.print_error();
301                e.address_book.print_all();
302                Err(anyhow::anyhow!("Unexpected failed transaction: {}", e))
303            }
304        }
305    }
306}