Skip to content

Commit 8347f63

Browse files
authored
feat: unit testing coverage (#190)
* feat(tests): (PRO-163) massive rehaul of mocking for unit testing, in order to reach the coveted 80% coverage - Added `redis-test` crate for improved testing of caching functionality. - Refactored `CacheUtil` to enhance account caching methods, including reintroducing `get_account_from_rpc_and_cache` and adding `is_cache_enabled` method. - Updated tests to utilize new mock structures for Redis caching, ensuring robust validation of caching behavior. - Cleaned up unused methods and improved overall code organization in cache-related files. * Cache tests * Config tests * Error tests * Fee tests * Balance tests * RPC methods unit test * Signer test (part 1) * RPC tests * Transaction tests * Signers external call tests * Tokens Tests * Rust CI update
1 parent ae883b5 commit 8347f63

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+7489
-1721
lines changed

.github/workflows/rust.yml

Lines changed: 34 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ env:
2222

2323
jobs:
2424
test:
25-
name: Rust Unit Tests
25+
name: Rust Tests & Coverage
2626
runs-on: ubuntu-latest
27+
timeout-minutes: 30
28+
permissions:
29+
contents: read
30+
pull-requests: write
2731
steps:
2832
- uses: actions/checkout@v4
2933
- uses: dtolnay/rust-toolchain@stable
@@ -34,33 +38,9 @@ jobs:
3438
run: make check
3539
- name: Run clippy
3640
run: make lint
37-
- name: Run tests
38-
run: make test
3941
- name: Build
4042
run: make build
41-
42-
integration-test:
43-
name: Integration Tests
44-
runs-on: ubuntu-latest
45-
needs: test
46-
timeout-minutes: 30
47-
permissions:
48-
contents: read
49-
pull-requests: write
50-
steps:
51-
- name: Checkout repository
52-
uses: actions/checkout@v4
53-
54-
- name: Setup Rust toolchain
55-
uses: dtolnay/rust-toolchain@stable
56-
with:
57-
components: llvm-tools-preview
58-
59-
- name: Cache Rust dependencies
60-
uses: Swatinem/rust-cache@v2
61-
with:
62-
cache-on-failure: true
63-
43+
6444
- name: Setup Solana CLI
6545
uses: ./.github/actions/setup-solana
6646

@@ -70,10 +50,11 @@ jobs:
7050
- name: Install cargo-llvm-cov for coverage
7151
run: cargo install cargo-llvm-cov
7252

73-
- name: Initialize coverage
53+
- name: Initialize coverage with unit tests
7454
run: |
75-
echo "🧪 Initializing coverage instrumentation..."
55+
echo "🧪 Running unit tests with coverage instrumentation..."
7656
cargo llvm-cov clean --workspace
57+
cargo llvm-cov test --no-report --workspace --lib
7758
7859
- name: Setup test environment
7960
run: |
@@ -169,7 +150,7 @@ jobs:
169150
path: coverage/
170151
retention-days: 30
171152

172-
- name: Post coverage comment on PR
153+
- name: Update PR description with coverage badge
173154
if: github.event_name == 'pull_request'
174155
uses: actions/github-script@v7
175156
with:
@@ -187,46 +168,38 @@ jobs:
187168
console.log('Error reading coverage:', error);
188169
}
189170
190-
const comment = `## 📊 Rust Coverage Report
191-
192-
**Coverage:** ${coverage}%
171+
// Determine badge color
172+
let color = 'red';
173+
if (parseFloat(coverage) >= 80) color = 'green';
174+
else if (parseFloat(coverage) >= 60) color = 'yellow';
193175
194-
<details>
195-
<summary>View detailed report</summary>
176+
// Get current PR
177+
const { data: pr } = await github.rest.pulls.get({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
pull_number: context.issue.number,
181+
});
196182
197-
Coverage artifacts have been uploaded to this workflow run.
198-
[View Artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
183+
// Create coverage badge section
184+
const coverageBadge = `![Coverage](https://img.shields.io/badge/coverage-${coverage}%25-${color})`;
185+
const coverageSection = `\n\n## 📊 Test Coverage\n${coverageBadge}\n\n**Coverage: ${coverage}%**\n\n[View Detailed Coverage Report](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`;
199186
200-
</details>`;
187+
// Update PR body
188+
let newBody = pr.body || '';
189+
190+
// Remove existing coverage section if present
191+
newBody = newBody.replace(/\n## 📊 Test Coverage[\s\S]*?(?=\n## |\n$|$)/g, '');
192+
193+
// Add new coverage section
194+
newBody += coverageSection;
201195
202-
// Find existing comment
203-
const { data: comments } = await github.rest.issues.listComments({
196+
await github.rest.pulls.update({
204197
owner: context.repo.owner,
205198
repo: context.repo.repo,
206-
issue_number: context.issue.number,
199+
pull_number: context.issue.number,
200+
body: newBody
207201
});
208202
209-
const botComment = comments.find(comment =>
210-
comment.user.type === 'Bot' &&
211-
comment.body.includes('## 📊 Rust Coverage Report')
212-
);
213-
214-
if (botComment) {
215-
await github.rest.issues.updateComment({
216-
owner: context.repo.owner,
217-
repo: context.repo.repo,
218-
comment_id: botComment.id,
219-
body: comment
220-
});
221-
} else {
222-
await github.rest.issues.createComment({
223-
owner: context.repo.owner,
224-
repo: context.repo.repo,
225-
issue_number: context.issue.number,
226-
body: comment
227-
});
228-
}
229-
230203
- name: Cleanup test environment
231204
uses: ./.github/actions/cleanup-test-env
232205

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ test-ledger/
2525

2626
# Docs
2727
sdks/ts/docs-html/
28-
sdks/ts/docs-md/
28+
sdks/ts/docs-md/
29+
30+
# AI
31+
.claude/helpers/

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,4 @@ tempfile = "3.2"
8585
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
8686
mockito = "1.2.0"
8787
serial_test = "3.2.0"
88+
redis-test = "0.12.0"

crates/lib/src/admin/token_util.rs

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use crate::{
22
error::KoraError,
33
signer::{KoraSigner, Signer},
4-
state::{get_all_signers, get_config, get_request_signer_with_signer_key},
4+
state::{get_all_signers, get_request_signer_with_signer_key},
55
token::token::TokenType,
66
transaction::TransactionUtil,
7-
CacheUtil,
87
};
98
use solana_client::nonblocking::rpc_client::RpcClient;
109
use solana_message::{Message, VersionedMessage};
@@ -18,6 +17,15 @@ use spl_associated_token_account::{
1817
};
1918
use std::{str::FromStr, sync::Arc};
2019

20+
#[cfg(not(test))]
21+
use {crate::cache::CacheUtil, crate::state::get_config};
22+
23+
#[cfg(test)]
24+
use {
25+
crate::tests::config_mock::mock_state::get_config,
26+
crate::tests::redis_cache_mock::MockCacheUtil as CacheUtil,
27+
};
28+
2129
/*
2230
This funciton is tested via the makefile, as it's a CLI command and requires a validator running.
2331
*/
@@ -261,3 +269,132 @@ pub async fn find_missing_atas(
261269

262270
Ok(atas_to_create)
263271
}
272+
273+
#[cfg(test)]
274+
mod tests {
275+
use super::*;
276+
use crate::tests::{
277+
common::{
278+
create_mock_rpc_client_account_not_found, create_mock_spl_mint_account,
279+
create_mock_token_account, setup_or_get_test_signer, RpcMockBuilder,
280+
},
281+
config_mock::{ConfigMockBuilder, ValidationConfigBuilder},
282+
};
283+
use std::{
284+
collections::VecDeque,
285+
sync::{Arc, Mutex},
286+
};
287+
288+
#[tokio::test]
289+
async fn test_find_missing_atas_no_spl_tokens() {
290+
let _m = ConfigMockBuilder::new()
291+
.with_validation(
292+
ValidationConfigBuilder::new().with_allowed_spl_paid_tokens(vec![]).build(),
293+
)
294+
.build_and_setup();
295+
296+
let rpc_client = create_mock_rpc_client_account_not_found();
297+
let payment_address = Pubkey::new_unique();
298+
299+
let result = find_missing_atas(&rpc_client, &payment_address).await.unwrap();
300+
301+
assert!(result.is_empty(), "Should return empty vec when no SPL tokens configured");
302+
}
303+
304+
#[tokio::test]
305+
async fn test_find_missing_atas_with_spl_tokens() {
306+
let allowed_spl_tokens = [Pubkey::new_unique(), Pubkey::new_unique()];
307+
308+
let _m = ConfigMockBuilder::new()
309+
.with_validation(
310+
ValidationConfigBuilder::new()
311+
.with_allowed_spl_paid_tokens(
312+
allowed_spl_tokens.iter().map(|p| p.to_string()).collect(),
313+
)
314+
.build(),
315+
)
316+
.build_and_setup();
317+
318+
let cache_ctx = CacheUtil::get_account_context();
319+
cache_ctx.checkpoint(); // Clear any previous expectations
320+
321+
let payment_address = Pubkey::new_unique();
322+
let rpc_client = create_mock_rpc_client_account_not_found();
323+
324+
// First call: Found in cache (Ok)
325+
// Second call: ATA account not found (Err)
326+
// Third call: mint account found (Ok)
327+
let responses = Arc::new(Mutex::new(VecDeque::from([
328+
Ok(create_mock_token_account(&Pubkey::new_unique(), &Pubkey::new_unique())),
329+
Err(KoraError::RpcError("ATA not found".to_string())),
330+
Ok(create_mock_spl_mint_account(6)),
331+
])));
332+
333+
let responses_clone = responses.clone();
334+
cache_ctx
335+
.expect()
336+
.times(3)
337+
.returning(move |_, _, _| responses_clone.lock().unwrap().pop_front().unwrap());
338+
339+
let result = find_missing_atas(&rpc_client, &payment_address).await;
340+
341+
assert!(result.is_ok(), "Should handle SPL tokens with proper mocking");
342+
let atas = result.unwrap();
343+
assert_eq!(atas.len(), 1, "Should return 1 missing ATAs");
344+
}
345+
346+
#[tokio::test]
347+
async fn test_create_atas_for_signer_calls_rpc_correctly() {
348+
let _m = ConfigMockBuilder::new().build_and_setup();
349+
350+
let _ = setup_or_get_test_signer();
351+
352+
let address = Pubkey::new_unique();
353+
let mint1 = Pubkey::new_unique();
354+
let mint2 = Pubkey::new_unique();
355+
356+
let atas_to_create = vec![
357+
ATAToCreate {
358+
mint: mint1,
359+
ata: spl_associated_token_account::get_associated_token_address(&address, &mint1),
360+
token_program: spl_token::id(),
361+
},
362+
ATAToCreate {
363+
mint: mint2,
364+
ata: spl_associated_token_account::get_associated_token_address(&address, &mint2),
365+
token_program: spl_token::id(),
366+
},
367+
];
368+
369+
let rpc_client = RpcMockBuilder::new().with_blockhash().with_send_transaction().build();
370+
371+
let result = create_atas_for_signer(
372+
&rpc_client,
373+
&get_request_signer_with_signer_key(None).unwrap(),
374+
&address,
375+
&atas_to_create,
376+
Some(1000),
377+
Some(100_000),
378+
2,
379+
)
380+
.await;
381+
382+
// Should fail with signature validation error since mock signature doesn't match real transaction
383+
match result {
384+
Ok(_) => {
385+
panic!("Expected signature validation error, but got success");
386+
}
387+
Err(e) => {
388+
let error_msg = format!("{e:?}");
389+
// Check if it's a signature validation error (the mocked signature doesn't match the real transaction signature)
390+
assert!(
391+
error_msg.contains("signature")
392+
|| error_msg.contains("Signature")
393+
|| error_msg.contains("invalid")
394+
|| error_msg.contains("mismatch"),
395+
"Expected signature validation error, got: {error_msg}"
396+
);
397+
}
398+
}
399+
}
400+
}

0 commit comments

Comments
 (0)