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}