From 55b25d0433f6b06b81da5cfc5d0d77a2653050b0 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:56:19 +0100 Subject: [PATCH 01/15] feat: add unit test suite and Docker CI workflow Add 53 unit tests covering atlas-common (error codes, pagination) and atlas-indexer (batch operations, block collection, metadata resolution). Add GitHub Actions workflow to build backend and frontend Docker images. --- .github/workflows/docker.yml | 60 +++++ backend/crates/atlas-common/src/error.rs | 55 ++++ backend/crates/atlas-common/src/types.rs | 78 ++++++ backend/crates/atlas-indexer/src/batch.rs | 204 +++++++++++++++ backend/crates/atlas-indexer/src/indexer.rs | 259 +++++++++++++++++++ backend/crates/atlas-indexer/src/metadata.rs | 62 +++++ 6 files changed, 718 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..8737e50 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,60 @@ +name: Docker Build + +on: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-backend: + name: Backend Docker (linux/amd64) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build indexer image + uses: docker/build-push-action@v6 + with: + context: backend + target: indexer + platforms: linux/amd64 + outputs: type=cacheonly + + - name: Build api image + uses: docker/build-push-action@v6 + with: + context: backend + target: api + platforms: linux/amd64 + # Reuses the builder stage cache from the indexer build above + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=cacheonly + + build-frontend: + name: Frontend Docker (linux/amd64) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build frontend image + uses: docker/build-push-action@v6 + with: + context: frontend + platforms: linux/amd64 + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=cacheonly diff --git a/backend/crates/atlas-common/src/error.rs b/backend/crates/atlas-common/src/error.rs index fe2741c..2b4ce8d 100644 --- a/backend/crates/atlas-common/src/error.rs +++ b/backend/crates/atlas-common/src/error.rs @@ -50,3 +50,58 @@ impl AtlasError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn not_found_returns_404() { + assert_eq!(AtlasError::NotFound("resource".into()).status_code(), 404); + } + + #[test] + fn invalid_input_returns_400() { + assert_eq!(AtlasError::InvalidInput("bad input".into()).status_code(), 400); + } + + #[test] + fn unauthorized_returns_401() { + assert_eq!(AtlasError::Unauthorized("no key".into()).status_code(), 401); + } + + #[test] + fn internal_error_returns_500() { + assert_eq!(AtlasError::Internal("oops".into()).status_code(), 500); + } + + #[test] + fn rpc_error_returns_502() { + assert_eq!(AtlasError::Rpc("timeout".into()).status_code(), 502); + } + + #[test] + fn metadata_fetch_returns_502() { + assert_eq!(AtlasError::MetadataFetch("ipfs down".into()).status_code(), 502); + } + + #[test] + fn config_error_returns_500() { + assert_eq!(AtlasError::Config("missing env".into()).status_code(), 500); + } + + #[test] + fn verification_error_returns_400() { + assert_eq!(AtlasError::Verification("bad source".into()).status_code(), 400); + } + + #[test] + fn bytecode_mismatch_returns_400() { + assert_eq!(AtlasError::BytecodeMismatch("different".into()).status_code(), 400); + } + + #[test] + fn compilation_error_returns_422() { + assert_eq!(AtlasError::Compilation("syntax error".into()).status_code(), 422); + } +} diff --git a/backend/crates/atlas-common/src/types.rs b/backend/crates/atlas-common/src/types.rs index a3f9776..861b723 100644 --- a/backend/crates/atlas-common/src/types.rs +++ b/backend/crates/atlas-common/src/types.rs @@ -401,3 +401,81 @@ impl PaginatedResponse { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn limit_above_max_clamps_to_100() { + let p = Pagination { page: 1, limit: 150 }; + assert_eq!(p.limit(), 100); + } + + #[test] + fn limit_at_max_is_unchanged() { + let p = Pagination { page: 1, limit: 100 }; + assert_eq!(p.limit(), 100); + } + + #[test] + fn limit_below_max_is_unchanged() { + let p = Pagination { page: 1, limit: 20 }; + assert_eq!(p.limit(), 20); + } + + #[test] + fn limit_zero_is_unchanged() { + let p = Pagination { page: 1, limit: 0 }; + assert_eq!(p.limit(), 0); + } + + #[test] + fn limit_u32_max_clamps_to_100() { + let p = Pagination { page: 1, limit: u32::MAX }; + assert_eq!(p.limit(), 100); + } + + #[test] + fn offset_page_zero_saturates_to_zero() { + // page=0 → saturating_sub(1)=0 → offset = 0 * limit = 0 + let p = Pagination { page: 0, limit: 20 }; + assert_eq!(p.offset(), 0); + } + + #[test] + fn offset_page_one_is_zero() { + let p = Pagination { page: 1, limit: 20 }; + assert_eq!(p.offset(), 0); + } + + #[test] + fn offset_page_two() { + let p = Pagination { page: 2, limit: 20 }; + assert_eq!(p.offset(), 20); + } + + #[test] + fn offset_page_three() { + let p = Pagination { page: 3, limit: 10 }; + assert_eq!(p.offset(), 20); + } + + #[test] + fn paginated_response_total_pages_rounds_up() { + let resp = PaginatedResponse::new(vec![1, 2, 3], 1, 10, 25); + assert_eq!(resp.total_pages, 3); + } + + #[test] + fn paginated_response_exact_division() { + let resp = PaginatedResponse::::new(vec![], 1, 10, 20); + assert_eq!(resp.total_pages, 2); + } + + #[test] + fn paginated_response_zero_total() { + let resp = PaginatedResponse::::new(vec![], 1, 10, 0); + assert_eq!(resp.total_pages, 0); + } +} diff --git a/backend/crates/atlas-indexer/src/batch.rs b/backend/crates/atlas-indexer/src/batch.rs index a3e8ffd..e9c14bf 100644 --- a/backend/crates/atlas-indexer/src/batch.rs +++ b/backend/crates/atlas-indexer/src/batch.rs @@ -155,3 +155,207 @@ impl BlockBatch { entry.last_block = entry.last_block.max(block); } } + +#[cfg(test)] +mod tests { + use super::*; + use bigdecimal::BigDecimal; + + // --- touch_addr tests --- + + #[test] + fn touch_addr_first_insertion() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xabc".to_string(), 100, false, 1); + + let state = batch.addr_map.get("0xabc").unwrap(); + assert_eq!(state.first_seen_block, 100); + assert!(!state.is_contract); + assert_eq!(state.tx_count_delta, 1); + } + + #[test] + fn touch_addr_keeps_minimum_first_seen_block() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xabc".to_string(), 200, false, 0); + batch.touch_addr("0xabc".to_string(), 100, false, 0); + + assert_eq!(batch.addr_map["0xabc"].first_seen_block, 100); + } + + #[test] + fn touch_addr_first_seen_block_does_not_increase() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xabc".to_string(), 100, false, 0); + batch.touch_addr("0xabc".to_string(), 200, false, 0); + + assert_eq!(batch.addr_map["0xabc"].first_seen_block, 100); + } + + #[test] + fn touch_addr_is_contract_latches_true() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xabc".to_string(), 100, false, 0); + batch.touch_addr("0xabc".to_string(), 101, true, 0); + + assert!(batch.addr_map["0xabc"].is_contract); + } + + #[test] + fn touch_addr_is_contract_once_true_stays_true() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xabc".to_string(), 100, true, 0); + batch.touch_addr("0xabc".to_string(), 101, false, 0); + + assert!(batch.addr_map["0xabc"].is_contract); + } + + #[test] + fn touch_addr_accumulates_tx_count_delta() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xabc".to_string(), 100, false, 1); + batch.touch_addr("0xabc".to_string(), 101, false, 2); + batch.touch_addr("0xabc".to_string(), 102, false, 3); + + assert_eq!(batch.addr_map["0xabc"].tx_count_delta, 6); + } + + #[test] + fn touch_addr_deduplicates_different_addresses_separately() { + let mut batch = BlockBatch::new(); + batch.touch_addr("0xaaa".to_string(), 100, false, 1); + batch.touch_addr("0xbbb".to_string(), 200, true, 2); + + assert_eq!(batch.addr_map.len(), 2); + assert_eq!(batch.addr_map["0xaaa"].tx_count_delta, 1); + assert_eq!(batch.addr_map["0xbbb"].tx_count_delta, 2); + assert!(!batch.addr_map["0xaaa"].is_contract); + assert!(batch.addr_map["0xbbb"].is_contract); + } + + // --- apply_balance_delta tests --- + + #[test] + fn apply_balance_delta_first_insertion() { + let mut batch = BlockBatch::new(); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(100), + 50, + ); + + let entry = batch + .balance_map + .get(&("0xaddr".to_string(), "0xtoken".to_string())) + .unwrap(); + assert_eq!(entry.delta, BigDecimal::from(100)); + assert_eq!(entry.last_block, 50); + } + + #[test] + fn apply_balance_delta_accumulates_positive() { + let mut batch = BlockBatch::new(); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(100), + 50, + ); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(50), + 60, + ); + + let entry = batch + .balance_map + .get(&("0xaddr".to_string(), "0xtoken".to_string())) + .unwrap(); + assert_eq!(entry.delta, BigDecimal::from(150)); + assert_eq!(entry.last_block, 60); + } + + #[test] + fn apply_balance_delta_accumulates_negative() { + let mut batch = BlockBatch::new(); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(100), + 50, + ); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(-30), + 51, + ); + + let entry = batch + .balance_map + .get(&("0xaddr".to_string(), "0xtoken".to_string())) + .unwrap(); + assert_eq!(entry.delta, BigDecimal::from(70)); + } + + #[test] + fn apply_balance_delta_tracks_max_block() { + let mut batch = BlockBatch::new(); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(1), + 100, + ); + // Earlier block — last_block should stay at 100 + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken".to_string(), + BigDecimal::from(1), + 50, + ); + + let entry = batch + .balance_map + .get(&("0xaddr".to_string(), "0xtoken".to_string())) + .unwrap(); + assert_eq!(entry.last_block, 100); + } + + #[test] + fn apply_balance_delta_separate_contracts_are_independent() { + let mut batch = BlockBatch::new(); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken1".to_string(), + BigDecimal::from(100), + 50, + ); + batch.apply_balance_delta( + "0xaddr".to_string(), + "0xtoken2".to_string(), + BigDecimal::from(200), + 50, + ); + + assert_eq!(batch.balance_map.len(), 2); + assert_eq!( + batch + .balance_map + .get(&("0xaddr".to_string(), "0xtoken1".to_string())) + .unwrap() + .delta, + BigDecimal::from(100) + ); + assert_eq!( + batch + .balance_map + .get(&("0xaddr".to_string(), "0xtoken2".to_string())) + .unwrap() + .delta, + BigDecimal::from(200) + ); + } +} diff --git a/backend/crates/atlas-indexer/src/indexer.rs b/backend/crates/atlas-indexer/src/indexer.rs index f2c26c3..f31746d 100644 --- a/backend/crates/atlas-indexer/src/indexer.rs +++ b/backend/crates/atlas-indexer/src/indexer.rs @@ -906,3 +906,262 @@ impl Indexer { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn empty_fetched_block(number: u64) -> FetchedBlock { + FetchedBlock { + number, + block: alloy::rpc::types::Block::default(), + receipts: vec![], + } + } + + fn make_receipt(logs_json: serde_json::Value) -> alloy::rpc::types::TransactionReceipt { + let receipt_json = serde_json::json!({ + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockNumber": "0x1", + "from": "0x0000000000000000000000000000000000000001", + "to": "0x0000000000000000000000000000000000000002", + "cumulativeGasUsed": "0x5208", + "gasUsed": "0x5208", + "contractAddress": null, + "logs": logs_json, + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "0x2", + "effectiveGasPrice": "0x1", + "status": "0x1" + }); + serde_json::from_value(receipt_json).expect("valid receipt JSON") + } + + #[test] + fn collect_empty_block_populates_block_fields() { + let mut batch = BlockBatch::new(); + let known_erc20 = HashSet::new(); + let known_nft = HashSet::new(); + + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, empty_fetched_block(100)); + + assert_eq!(batch.b_numbers, vec![100i64]); + assert_eq!(batch.b_tx_counts, vec![0i32]); + assert!(batch.addr_map.is_empty()); + assert_eq!(batch.last_block, 100); + } + + #[test] + fn collect_multiple_blocks_accumulate_in_order() { + let mut batch = BlockBatch::new(); + let known_erc20 = HashSet::new(); + let known_nft = HashSet::new(); + + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, empty_fetched_block(1)); + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, empty_fetched_block(2)); + + assert_eq!(batch.b_numbers, vec![1i64, 2i64]); + assert_eq!(batch.last_block, 2); + } + + #[test] + fn collect_erc20_transfer_populates_transfer_and_balance_arrays() { + let mut batch = BlockBatch::new(); + let known_erc20 = HashSet::new(); + let known_nft = HashSet::new(); + + // ERC-20 Transfer: 3 topics + 32 bytes data (value = 1000) + let logs = serde_json::json!([{ + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000001111111111111111111111111111111111111111", + "0x0000000000000000000000002222222222222222222222222222222222222222" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000003e8", + "blockNumber": "0x1", + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": "0x0", + "removed": false + }]); + + let mut fb = empty_fetched_block(1); + fb.receipts = vec![make_receipt(logs)]; + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, fb); + + assert_eq!(batch.et_contracts.len(), 1); + assert_eq!(batch.et_froms.len(), 1); + assert_eq!(batch.et_tos.len(), 1); + assert_eq!(batch.et_values, vec!["1000".to_string()]); + + // New ERC-20 contract registered + assert_eq!(batch.ec_addresses.len(), 1); + assert_eq!(batch.new_erc20.len(), 1); + + // Two balance deltas: sender (negative) and receiver (positive) + assert_eq!(batch.balance_map.len(), 2); + + let contract = batch.ec_addresses[0].clone(); + let from = "0x1111111111111111111111111111111111111111"; + let to = "0x2222222222222222222222222222222222222222"; + + let sender_delta = &batch.balance_map[&(from.to_string(), contract.clone())]; + assert!(sender_delta.delta < 0); + + let receiver_delta = &batch.balance_map[&(to.to_string(), contract)]; + assert!(receiver_delta.delta > 0); + } + + #[test] + fn collect_erc20_mint_skips_zero_address_balance_delta() { + let mut batch = BlockBatch::new(); + let known_erc20 = HashSet::new(); + let known_nft = HashSet::new(); + + // Mint: from = ZERO_ADDRESS → no balance delta for sender + let logs = serde_json::json!([{ + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000002222222222222222222222222222222222222222" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000003e8", + "blockNumber": "0x1", + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": "0x0", + "removed": false + }]); + + let mut fb = empty_fetched_block(1); + fb.receipts = vec![make_receipt(logs)]; + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, fb); + + // Only the receiver gets a balance delta; zero address is excluded + assert_eq!(batch.balance_map.len(), 1); + let contract = batch.ec_addresses[0].clone(); + let to = "0x2222222222222222222222222222222222222222"; + assert!(batch + .balance_map + .contains_key(&(to.to_string(), contract))); + } + + #[test] + fn collect_erc20_known_contract_not_added_to_ec_addresses() { + let mut batch = BlockBatch::new(); + let mut known_erc20 = HashSet::new(); + known_erc20.insert("0x3333333333333333333333333333333333333333".to_string()); + let known_nft = HashSet::new(); + + let logs = serde_json::json!([{ + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000001111111111111111111111111111111111111111", + "0x0000000000000000000000002222222222222222222222222222222222222222" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000003e8", + "blockNumber": "0x1", + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": "0x0", + "removed": false + }]); + + let mut fb = empty_fetched_block(1); + fb.receipts = vec![make_receipt(logs)]; + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, fb); + + // Transfer is still recorded + assert_eq!(batch.et_contracts.len(), 1); + // But contract is NOT added again (already in known_erc20) + assert_eq!(batch.ec_addresses.len(), 0); + assert_eq!(batch.new_erc20.len(), 0); + } + + #[test] + fn collect_erc721_transfer_populates_nft_arrays() { + let mut batch = BlockBatch::new(); + let known_erc20 = HashSet::new(); + let known_nft = HashSet::new(); + + // ERC-721 Transfer: 4 topics, token ID = 42, empty data + let logs = serde_json::json!([{ + "address": "0x4444444444444444444444444444444444444444", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000001111111111111111111111111111111111111111", + "0x0000000000000000000000002222222222222222222222222222222222222222", + "0x000000000000000000000000000000000000000000000000000000000000002a" + ], + "data": "0x", + "blockNumber": "0x1", + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": "0x0", + "removed": false + }]); + + let mut fb = empty_fetched_block(1); + fb.receipts = vec![make_receipt(logs)]; + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, fb); + + assert_eq!(batch.nt_contracts.len(), 1); + assert_eq!(batch.nt_token_ids, vec!["42".to_string()]); + assert_eq!(batch.nt_froms.len(), 1); + assert_eq!(batch.nt_tos.len(), 1); + + // New NFT contract registered + assert_eq!(batch.nft_contract_addrs.len(), 1); + assert_eq!(batch.new_nft.len(), 1); + + // NFT token ownership tracked in nft_token_map + assert_eq!(batch.nft_token_map.len(), 1); + + // No ERC-20 data and no balance deltas + assert!(batch.et_contracts.is_empty()); + assert!(batch.balance_map.is_empty()); + } + + #[test] + fn collect_ambiguous_transfer_skipped_when_data_too_short() { + let mut batch = BlockBatch::new(); + let known_erc20 = HashSet::new(); + let known_nft = HashSet::new(); + + // 3 topics but only 2 bytes of data → neither ERC-20 nor ERC-721 + let logs = serde_json::json!([{ + "address": "0x3333333333333333333333333333333333333333", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000001111111111111111111111111111111111111111", + "0x0000000000000000000000002222222222222222222222222222222222222222" + ], + "data": "0x1234", + "blockNumber": "0x1", + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "transactionIndex": "0x0", + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": "0x0", + "removed": false + }]); + + let mut fb = empty_fetched_block(1); + fb.receipts = vec![make_receipt(logs)]; + Indexer::collect_block(&mut batch, &known_erc20, &known_nft, fb); + + assert!(batch.et_contracts.is_empty()); + assert!(batch.nt_contracts.is_empty()); + assert!(batch.ec_addresses.is_empty()); + assert!(batch.nft_contract_addrs.is_empty()); + assert!(batch.balance_map.is_empty()); + } +} diff --git a/backend/crates/atlas-indexer/src/metadata.rs b/backend/crates/atlas-indexer/src/metadata.rs index 3af8cb1..935ccdc 100644 --- a/backend/crates/atlas-indexer/src/metadata.rs +++ b/backend/crates/atlas-indexer/src/metadata.rs @@ -536,3 +536,65 @@ fn resolve_uri(uri: &str, ipfs_gateway: &str) -> String { uri.to_string() } } + +#[cfg(test)] +mod tests { + use super::*; + + const GATEWAY: &str = "https://ipfs.io/ipfs/"; + + #[test] + fn resolve_ipfs_uri_prefixes_gateway() { + assert_eq!( + resolve_uri("ipfs://QmXxx123", GATEWAY), + "https://ipfs.io/ipfs/QmXxx123" + ); + } + + #[test] + fn resolve_ipfs_uri_with_path() { + assert_eq!( + resolve_uri("ipfs://QmXxx123/metadata/1.json", GATEWAY), + "https://ipfs.io/ipfs/QmXxx123/metadata/1.json" + ); + } + + #[test] + fn resolve_ipfs_uses_custom_gateway() { + assert_eq!( + resolve_uri("ipfs://QmXxx123", "https://cloudflare-ipfs.com/ipfs/"), + "https://cloudflare-ipfs.com/ipfs/QmXxx123" + ); + } + + #[test] + fn resolve_arweave_uri() { + assert_eq!( + resolve_uri("ar://txid123", GATEWAY), + "https://arweave.net/txid123" + ); + } + + #[test] + fn resolve_data_uri_is_unchanged() { + let data = "data:image/png;base64,abc123=="; + assert_eq!(resolve_uri(data, GATEWAY), data); + } + + #[test] + fn resolve_https_uri_is_unchanged() { + let url = "https://example.com/metadata/1.json"; + assert_eq!(resolve_uri(url, GATEWAY), url); + } + + #[test] + fn resolve_http_uri_is_unchanged() { + let url = "http://example.com/metadata/1.json"; + assert_eq!(resolve_uri(url, GATEWAY), url); + } + + #[test] + fn resolve_empty_string_is_unchanged() { + assert_eq!(resolve_uri("", GATEWAY), ""); + } +} From 6f9d5204cb033a51114ccaa60cca9cae48d5d19e Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:30:56 +0100 Subject: [PATCH 02/15] fix: apply cargo fmt formatting to test code --- backend/crates/atlas-common/src/error.rs | 25 ++++++++++++++++----- backend/crates/atlas-common/src/types.rs | 15 ++++++++++--- backend/crates/atlas-indexer/src/indexer.rs | 11 +++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/crates/atlas-common/src/error.rs b/backend/crates/atlas-common/src/error.rs index 2b4ce8d..175bc22 100644 --- a/backend/crates/atlas-common/src/error.rs +++ b/backend/crates/atlas-common/src/error.rs @@ -62,7 +62,10 @@ mod tests { #[test] fn invalid_input_returns_400() { - assert_eq!(AtlasError::InvalidInput("bad input".into()).status_code(), 400); + assert_eq!( + AtlasError::InvalidInput("bad input".into()).status_code(), + 400 + ); } #[test] @@ -82,7 +85,10 @@ mod tests { #[test] fn metadata_fetch_returns_502() { - assert_eq!(AtlasError::MetadataFetch("ipfs down".into()).status_code(), 502); + assert_eq!( + AtlasError::MetadataFetch("ipfs down".into()).status_code(), + 502 + ); } #[test] @@ -92,16 +98,25 @@ mod tests { #[test] fn verification_error_returns_400() { - assert_eq!(AtlasError::Verification("bad source".into()).status_code(), 400); + assert_eq!( + AtlasError::Verification("bad source".into()).status_code(), + 400 + ); } #[test] fn bytecode_mismatch_returns_400() { - assert_eq!(AtlasError::BytecodeMismatch("different".into()).status_code(), 400); + assert_eq!( + AtlasError::BytecodeMismatch("different".into()).status_code(), + 400 + ); } #[test] fn compilation_error_returns_422() { - assert_eq!(AtlasError::Compilation("syntax error".into()).status_code(), 422); + assert_eq!( + AtlasError::Compilation("syntax error".into()).status_code(), + 422 + ); } } diff --git a/backend/crates/atlas-common/src/types.rs b/backend/crates/atlas-common/src/types.rs index 861b723..c9d3ab2 100644 --- a/backend/crates/atlas-common/src/types.rs +++ b/backend/crates/atlas-common/src/types.rs @@ -408,13 +408,19 @@ mod tests { #[test] fn limit_above_max_clamps_to_100() { - let p = Pagination { page: 1, limit: 150 }; + let p = Pagination { + page: 1, + limit: 150, + }; assert_eq!(p.limit(), 100); } #[test] fn limit_at_max_is_unchanged() { - let p = Pagination { page: 1, limit: 100 }; + let p = Pagination { + page: 1, + limit: 100, + }; assert_eq!(p.limit(), 100); } @@ -432,7 +438,10 @@ mod tests { #[test] fn limit_u32_max_clamps_to_100() { - let p = Pagination { page: 1, limit: u32::MAX }; + let p = Pagination { + page: 1, + limit: u32::MAX, + }; assert_eq!(p.limit(), 100); } diff --git a/backend/crates/atlas-indexer/src/indexer.rs b/backend/crates/atlas-indexer/src/indexer.rs index f31746d..9994c1b 100644 --- a/backend/crates/atlas-indexer/src/indexer.rs +++ b/backend/crates/atlas-indexer/src/indexer.rs @@ -945,7 +945,12 @@ mod tests { let known_erc20 = HashSet::new(); let known_nft = HashSet::new(); - Indexer::collect_block(&mut batch, &known_erc20, &known_nft, empty_fetched_block(100)); + Indexer::collect_block( + &mut batch, + &known_erc20, + &known_nft, + empty_fetched_block(100), + ); assert_eq!(batch.b_numbers, vec![100i64]); assert_eq!(batch.b_tx_counts, vec![0i32]); @@ -1047,9 +1052,7 @@ mod tests { assert_eq!(batch.balance_map.len(), 1); let contract = batch.ec_addresses[0].clone(); let to = "0x2222222222222222222222222222222222222222"; - assert!(batch - .balance_map - .contains_key(&(to.to_string(), contract))); + assert!(batch.balance_map.contains_key(&(to.to_string(), contract))); } #[test] From d68916c6e1a851f3dd5395f933f9a5b853ab2295 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:51:23 +0100 Subject: [PATCH 03/15] ci: split backend into parallel fmt/clippy/test jobs Reduces CI wall time by running format check, clippy, and tests concurrently instead of sequentially. --- .github/workflows/ci.yml | 45 ++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebdb3b8..edcddd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ concurrency: cancel-in-progress: true jobs: - backend: - name: Backend (Rust) + backend-fmt: + name: Backend Format runs-on: ubuntu-latest defaults: run: @@ -24,19 +24,52 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: - components: rustfmt, clippy + components: rustfmt + + - name: Format + run: cargo fmt --all --check + + backend-clippy: + name: Backend Clippy + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy - name: Cache Cargo uses: Swatinem/rust-cache@v2 with: workspaces: backend - - name: Format - run: cargo fmt --all --check - - name: Clippy run: cargo clippy --workspace --all-targets -- -D warnings + backend-test: + name: Backend Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: backend + - name: Test run: cargo test --workspace --all-targets From b747aa8b6cd70fd38063468fb2144fe622faa78a Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:45:02 +0100 Subject: [PATCH 04/15] ci: parallelize Docker builds and add indexer cache Split indexer and api into separate parallel jobs. Add GHA cache to the indexer build which was previously rebuilding from scratch every run. --- .github/workflows/docker.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8737e50..e671c15 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,8 +11,8 @@ concurrency: cancel-in-progress: true jobs: - build-backend: - name: Backend Docker (linux/amd64) + build-indexer: + name: Indexer Docker (linux/amd64) runs-on: ubuntu-latest steps: - name: Checkout @@ -27,17 +27,28 @@ jobs: context: backend target: indexer platforms: linux/amd64 + cache-from: type=gha,scope=backend + cache-to: type=gha,scope=backend,mode=max outputs: type=cacheonly + build-api: + name: API Docker (linux/amd64) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build api image uses: docker/build-push-action@v6 with: context: backend target: api platforms: linux/amd64 - # Reuses the builder stage cache from the indexer build above - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=backend + cache-to: type=gha,scope=backend,mode=max outputs: type=cacheonly build-frontend: From 9a87f66974ffe5f5920da6b9706b6668cf854eca Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:58:41 +0100 Subject: [PATCH 05/15] ci: use cargo-chef to cache Docker dependency compilation Splits the Rust build into dependency cooking (cached) and source compilation, so code-only changes skip the expensive dependency build. --- backend/Dockerfile | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 07c84bb..050d0f9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,9 +1,23 @@ -# Build stage +# Compute a dependency recipe from the workspace manifests and lock file +FROM rust:1.85-alpine AS planner +RUN cargo install cargo-chef --locked +WORKDIR /app +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +# Build stage — cook dependencies first, then compile app code FROM rust:1.85-alpine AS builder RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig +RUN cargo install cargo-chef --locked WORKDIR /app + +# Cook dependencies (cached until Cargo.toml/Cargo.lock change) +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json + +# Copy source and build (only this layer rebuilds on code changes) COPY . . # Downgrade home crate to version compatible with Rust 1.85 From d5d10894c5fbd64e9b8b1e6e59581011e88d9791 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:05:31 +0100 Subject: [PATCH 06/15] fix: use pre-built cargo-chef image compatible with Rust 1.85 cargo-chef v0.1.77 requires rustc 1.86+ which is incompatible with the rust:1.85-alpine base image. Switch to the official lukemathwalker/cargo-chef Docker image which bundles a compatible cargo-chef binary. --- backend/Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 050d0f9..2622072 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,15 +1,13 @@ # Compute a dependency recipe from the workspace manifests and lock file -FROM rust:1.85-alpine AS planner -RUN cargo install cargo-chef --locked +FROM lukemathwalker/cargo-chef:latest-rust-1.85-alpine AS planner WORKDIR /app COPY . . RUN cargo chef prepare --recipe-path recipe.json # Build stage — cook dependencies first, then compile app code -FROM rust:1.85-alpine AS builder +FROM lukemathwalker/cargo-chef:latest-rust-1.85-alpine AS builder RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig -RUN cargo install cargo-chef --locked WORKDIR /app From 27b8a9fa99cb749e9e60055bf2865218046e7fd9 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:09:19 +0100 Subject: [PATCH 07/15] fix: run cargo update downgrades before cargo-chef prepare The version downgrades for Rust 1.85 compatibility (home, wit-bindgen, wasip2) must happen before preparing the recipe, otherwise cargo-chef cook resolves incompatible versions from the lockfile. --- backend/Dockerfile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 2622072..c4b6454 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,10 @@ FROM lukemathwalker/cargo-chef:latest-rust-1.85-alpine AS planner WORKDIR /app COPY . . +# Downgrade crates incompatible with Rust 1.85 before preparing the recipe +RUN cargo update home@0.5.12 --precise 0.5.9 || true +RUN cargo update wit-bindgen --precise 0.41.0 || true +RUN cargo update wasip2 --precise 0.1.0 || true RUN cargo chef prepare --recipe-path recipe.json # Build stage — cook dependencies first, then compile app code @@ -17,12 +21,6 @@ RUN cargo chef cook --release --recipe-path recipe.json # Copy source and build (only this layer rebuilds on code changes) COPY . . - -# Downgrade home crate to version compatible with Rust 1.85 -RUN cargo update home@0.5.12 --precise 0.5.9 || true -RUN cargo update wit-bindgen --precise 0.41.0 || true -RUN cargo update wasip2 --precise 0.1.0 || true - RUN cargo build --release # Indexer image From 4bad997ad86626be1dace6a0cc5cf239926b0c18 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:28:05 +0100 Subject: [PATCH 08/15] revert: remove cargo-chef, restore simple Dockerfile cargo-chef adds complexity with Rust 1.85 version pinning and doesn't improve build times on cold cache. GHA layer caching handles caching across runs already. --- backend/Dockerfile | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index c4b6454..07c84bb 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,26 +1,16 @@ -# Compute a dependency recipe from the workspace manifests and lock file -FROM lukemathwalker/cargo-chef:latest-rust-1.85-alpine AS planner -WORKDIR /app -COPY . . -# Downgrade crates incompatible with Rust 1.85 before preparing the recipe -RUN cargo update home@0.5.12 --precise 0.5.9 || true -RUN cargo update wit-bindgen --precise 0.41.0 || true -RUN cargo update wasip2 --precise 0.1.0 || true -RUN cargo chef prepare --recipe-path recipe.json - -# Build stage — cook dependencies first, then compile app code -FROM lukemathwalker/cargo-chef:latest-rust-1.85-alpine AS builder +# Build stage +FROM rust:1.85-alpine AS builder RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig WORKDIR /app +COPY . . -# Cook dependencies (cached until Cargo.toml/Cargo.lock change) -COPY --from=planner /app/recipe.json recipe.json -RUN cargo chef cook --release --recipe-path recipe.json +# Downgrade home crate to version compatible with Rust 1.85 +RUN cargo update home@0.5.12 --precise 0.5.9 || true +RUN cargo update wit-bindgen --precise 0.41.0 || true +RUN cargo update wasip2 --precise 0.1.0 || true -# Copy source and build (only this layer rebuilds on code changes) -COPY . . RUN cargo build --release # Indexer image From 4fe133891c2b656e345ee1d36c0360c8bebfbd59 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:29:49 +0100 Subject: [PATCH 09/15] docs: add unit testing convention to CLAUDE.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index dc7a8d6..d35bb60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,6 +89,7 @@ pub struct AppState { - **Migrations**: use `run_migrations(&database_url)` (not `&pool`) to get a timeout-free connection - **Frontend**: uses Bun (not npm/yarn). Lockfile is `bun.lock` (text, Bun ≥ 1.2). Build with `bunx vite build` (skips tsc type check). - **Docker**: frontend image uses `nginxinc/nginx-unprivileged:alpine` (non-root, port 8080). API/indexer use `alpine` with `ca-certificates`. +- **Tests**: add unit tests for new logic in a `#[cfg(test)] mod tests` block in the same file. Run with `cargo test --workspace`. - **Commits**: authored by the user only — no Claude co-author lines. ## Environment Variables From 1f3a95fe532eaa9957ced0396f4131fe40216ef0 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:45:23 +0100 Subject: [PATCH 10/15] ci: pin Docker workflow actions to commit SHAs and add Dependabot Pins actions/checkout, docker/setup-buildx-action, and docker/build-push-action to immutable commit SHAs to prevent supply-chain attacks via tag retargeting. Adds dependabot.yml to auto-update those SHAs weekly. --- .github/dependabot.yml | 6 ++++++ .github/workflows/docker.yml | 18 +++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e671c15..e5ea84b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,13 +16,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3 - name: Build indexer image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6 with: context: backend target: indexer @@ -36,13 +36,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3 - name: Build api image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6 with: context: backend target: api @@ -56,13 +56,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3 - name: Build frontend image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6 with: context: frontend platforms: linux/amd64 From 6c884c5ee3addbd46a4e3cea21caa38812d8b536 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:45:54 +0100 Subject: [PATCH 11/15] revert: use version tags instead of SHA pins for trusted actions --- .github/dependabot.yml | 6 ------ .github/workflows/docker.yml | 18 +++++++++--------- 2 files changed, 9 insertions(+), 15 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index ca79ca5..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e5ea84b..e671c15 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,13 +16,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 - name: Build indexer image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6 + uses: docker/build-push-action@v6 with: context: backend target: indexer @@ -36,13 +36,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 - name: Build api image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6 + uses: docker/build-push-action@v6 with: context: backend target: api @@ -56,13 +56,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # actions/checkout@v4 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v3 - name: Build frontend image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6 + uses: docker/build-push-action@v6 with: context: frontend platforms: linux/amd64 From 11f379d7b75d17ea6cb5c7aa98cd2b22444bb355 Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:37:32 +0100 Subject: [PATCH 12/15] test: remove low-value tests, keep tests that cover real logic --- backend/crates/atlas-common/src/error.rs | 37 -------------------- backend/crates/atlas-common/src/types.rs | 21 ----------- backend/crates/atlas-indexer/src/batch.rs | 18 ---------- backend/crates/atlas-indexer/src/metadata.rs | 14 -------- 4 files changed, 90 deletions(-) diff --git a/backend/crates/atlas-common/src/error.rs b/backend/crates/atlas-common/src/error.rs index 175bc22..5554666 100644 --- a/backend/crates/atlas-common/src/error.rs +++ b/backend/crates/atlas-common/src/error.rs @@ -60,14 +60,6 @@ mod tests { assert_eq!(AtlasError::NotFound("resource".into()).status_code(), 404); } - #[test] - fn invalid_input_returns_400() { - assert_eq!( - AtlasError::InvalidInput("bad input".into()).status_code(), - 400 - ); - } - #[test] fn unauthorized_returns_401() { assert_eq!(AtlasError::Unauthorized("no key".into()).status_code(), 401); @@ -83,35 +75,6 @@ mod tests { assert_eq!(AtlasError::Rpc("timeout".into()).status_code(), 502); } - #[test] - fn metadata_fetch_returns_502() { - assert_eq!( - AtlasError::MetadataFetch("ipfs down".into()).status_code(), - 502 - ); - } - - #[test] - fn config_error_returns_500() { - assert_eq!(AtlasError::Config("missing env".into()).status_code(), 500); - } - - #[test] - fn verification_error_returns_400() { - assert_eq!( - AtlasError::Verification("bad source".into()).status_code(), - 400 - ); - } - - #[test] - fn bytecode_mismatch_returns_400() { - assert_eq!( - AtlasError::BytecodeMismatch("different".into()).status_code(), - 400 - ); - } - #[test] fn compilation_error_returns_422() { assert_eq!( diff --git a/backend/crates/atlas-common/src/types.rs b/backend/crates/atlas-common/src/types.rs index c9d3ab2..c9ce22f 100644 --- a/backend/crates/atlas-common/src/types.rs +++ b/backend/crates/atlas-common/src/types.rs @@ -415,21 +415,6 @@ mod tests { assert_eq!(p.limit(), 100); } - #[test] - fn limit_at_max_is_unchanged() { - let p = Pagination { - page: 1, - limit: 100, - }; - assert_eq!(p.limit(), 100); - } - - #[test] - fn limit_below_max_is_unchanged() { - let p = Pagination { page: 1, limit: 20 }; - assert_eq!(p.limit(), 20); - } - #[test] fn limit_zero_is_unchanged() { let p = Pagination { page: 1, limit: 0 }; @@ -464,12 +449,6 @@ mod tests { assert_eq!(p.offset(), 20); } - #[test] - fn offset_page_three() { - let p = Pagination { page: 3, limit: 10 }; - assert_eq!(p.offset(), 20); - } - #[test] fn paginated_response_total_pages_rounds_up() { let resp = PaginatedResponse::new(vec![1, 2, 3], 1, 10, 25); diff --git a/backend/crates/atlas-indexer/src/batch.rs b/backend/crates/atlas-indexer/src/batch.rs index e9c14bf..db699da 100644 --- a/backend/crates/atlas-indexer/src/batch.rs +++ b/backend/crates/atlas-indexer/src/batch.rs @@ -183,15 +183,6 @@ mod tests { assert_eq!(batch.addr_map["0xabc"].first_seen_block, 100); } - #[test] - fn touch_addr_first_seen_block_does_not_increase() { - let mut batch = BlockBatch::new(); - batch.touch_addr("0xabc".to_string(), 100, false, 0); - batch.touch_addr("0xabc".to_string(), 200, false, 0); - - assert_eq!(batch.addr_map["0xabc"].first_seen_block, 100); - } - #[test] fn touch_addr_is_contract_latches_true() { let mut batch = BlockBatch::new(); @@ -201,15 +192,6 @@ mod tests { assert!(batch.addr_map["0xabc"].is_contract); } - #[test] - fn touch_addr_is_contract_once_true_stays_true() { - let mut batch = BlockBatch::new(); - batch.touch_addr("0xabc".to_string(), 100, true, 0); - batch.touch_addr("0xabc".to_string(), 101, false, 0); - - assert!(batch.addr_map["0xabc"].is_contract); - } - #[test] fn touch_addr_accumulates_tx_count_delta() { let mut batch = BlockBatch::new(); diff --git a/backend/crates/atlas-indexer/src/metadata.rs b/backend/crates/atlas-indexer/src/metadata.rs index 935ccdc..76e20ee 100644 --- a/backend/crates/atlas-indexer/src/metadata.rs +++ b/backend/crates/atlas-indexer/src/metadata.rs @@ -559,14 +559,6 @@ mod tests { ); } - #[test] - fn resolve_ipfs_uses_custom_gateway() { - assert_eq!( - resolve_uri("ipfs://QmXxx123", "https://cloudflare-ipfs.com/ipfs/"), - "https://cloudflare-ipfs.com/ipfs/QmXxx123" - ); - } - #[test] fn resolve_arweave_uri() { assert_eq!( @@ -587,12 +579,6 @@ mod tests { assert_eq!(resolve_uri(url, GATEWAY), url); } - #[test] - fn resolve_http_uri_is_unchanged() { - let url = "http://example.com/metadata/1.json"; - assert_eq!(resolve_uri(url, GATEWAY), url); - } - #[test] fn resolve_empty_string_is_unchanged() { assert_eq!(resolve_uri("", GATEWAY), ""); From 0027e59c01154012f9ff7e1739425d05bcd2e1ca Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:52:15 +0100 Subject: [PATCH 13/15] test: remove tests that only verify static mappings and stdlib behavior --- backend/crates/atlas-common/src/error.rs | 32 ------------------------ backend/crates/atlas-common/src/types.rs | 24 ------------------ 2 files changed, 56 deletions(-) diff --git a/backend/crates/atlas-common/src/error.rs b/backend/crates/atlas-common/src/error.rs index 5554666..7d29551 100644 --- a/backend/crates/atlas-common/src/error.rs +++ b/backend/crates/atlas-common/src/error.rs @@ -51,35 +51,3 @@ impl AtlasError { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn not_found_returns_404() { - assert_eq!(AtlasError::NotFound("resource".into()).status_code(), 404); - } - - #[test] - fn unauthorized_returns_401() { - assert_eq!(AtlasError::Unauthorized("no key".into()).status_code(), 401); - } - - #[test] - fn internal_error_returns_500() { - assert_eq!(AtlasError::Internal("oops".into()).status_code(), 500); - } - - #[test] - fn rpc_error_returns_502() { - assert_eq!(AtlasError::Rpc("timeout".into()).status_code(), 502); - } - - #[test] - fn compilation_error_returns_422() { - assert_eq!( - AtlasError::Compilation("syntax error".into()).status_code(), - 422 - ); - } -} diff --git a/backend/crates/atlas-common/src/types.rs b/backend/crates/atlas-common/src/types.rs index c9ce22f..cfe5847 100644 --- a/backend/crates/atlas-common/src/types.rs +++ b/backend/crates/atlas-common/src/types.rs @@ -406,30 +406,6 @@ impl PaginatedResponse { mod tests { use super::*; - #[test] - fn limit_above_max_clamps_to_100() { - let p = Pagination { - page: 1, - limit: 150, - }; - assert_eq!(p.limit(), 100); - } - - #[test] - fn limit_zero_is_unchanged() { - let p = Pagination { page: 1, limit: 0 }; - assert_eq!(p.limit(), 0); - } - - #[test] - fn limit_u32_max_clamps_to_100() { - let p = Pagination { - page: 1, - limit: u32::MAX, - }; - assert_eq!(p.limit(), 100); - } - #[test] fn offset_page_zero_saturates_to_zero() { // page=0 → saturating_sub(1)=0 → offset = 0 * limit = 0 From 90b8adcb757155c8549297aea0a2ff82fcdc162d Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:55:23 +0100 Subject: [PATCH 14/15] fix: remove trailing blank line in error.rs after test block removal --- backend/crates/atlas-common/src/error.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/crates/atlas-common/src/error.rs b/backend/crates/atlas-common/src/error.rs index 7d29551..fe2741c 100644 --- a/backend/crates/atlas-common/src/error.rs +++ b/backend/crates/atlas-common/src/error.rs @@ -50,4 +50,3 @@ impl AtlasError { } } } - From 87521b54c3d24656e097f90b925ea5d0802085cc Mon Sep 17 00:00:00 2001 From: pthmas <9058370+pthmas@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:13:10 +0100 Subject: [PATCH 15/15] test: remove offset and pagination tests that only verify stdlib arithmetic --- backend/crates/atlas-common/src/types.rs | 42 ------------------------ 1 file changed, 42 deletions(-) diff --git a/backend/crates/atlas-common/src/types.rs b/backend/crates/atlas-common/src/types.rs index cfe5847..a3f9776 100644 --- a/backend/crates/atlas-common/src/types.rs +++ b/backend/crates/atlas-common/src/types.rs @@ -401,45 +401,3 @@ impl PaginatedResponse { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn offset_page_zero_saturates_to_zero() { - // page=0 → saturating_sub(1)=0 → offset = 0 * limit = 0 - let p = Pagination { page: 0, limit: 20 }; - assert_eq!(p.offset(), 0); - } - - #[test] - fn offset_page_one_is_zero() { - let p = Pagination { page: 1, limit: 20 }; - assert_eq!(p.offset(), 0); - } - - #[test] - fn offset_page_two() { - let p = Pagination { page: 2, limit: 20 }; - assert_eq!(p.offset(), 20); - } - - #[test] - fn paginated_response_total_pages_rounds_up() { - let resp = PaginatedResponse::new(vec![1, 2, 3], 1, 10, 25); - assert_eq!(resp.total_pages, 3); - } - - #[test] - fn paginated_response_exact_division() { - let resp = PaginatedResponse::::new(vec![], 1, 10, 20); - assert_eq!(resp.total_pages, 2); - } - - #[test] - fn paginated_response_zero_total() { - let resp = PaginatedResponse::::new(vec![], 1, 10, 0); - assert_eq!(resp.total_pages, 0); - } -}