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}