Skip to content

test: expand grpc service unit test coverage#2

Merged
thlorenz merged 14 commits into
masterfrom
tests
Apr 22, 2026
Merged

test: expand grpc service unit test coverage#2
thlorenz merged 14 commits into
masterfrom
tests

Conversation

@thlorenz
Copy link
Copy Markdown
Collaborator

@thlorenz thlorenz commented Apr 22, 2026

Summary

Refactors grpc-service to inject its external collaborators through trait-based seams and
adds a focused unit-test suite around the newly testable paths. The PR covers snapshot loading,
Kafka payload decoding, gRPC bootstrap behavior, dispatcher routing and health policy, app
orchestration, and output formatting without introducing networked or integration-style tests.

Details

Testability Refactor

  • adds collaborator traits for snapshot access, validator subscription updates, and account update streaming
  • updates App and GrpcSubscriptionService to accept injected implementations while preserving the existing production constructors and runtime wiring
  • keeps the gRPC bootstrap flow behavior intact while making snapshot fetches and validator whitelist calls replaceable in tests

Unit Test Coverage

  • adds deterministic tests for domain parsing and conversion helpers
  • adds exhaustive pure tests for ksql row parsing, response parsing, and decode edge cases
  • adds Kafka payload decoding coverage for raw protobuf, wrapped protobuf, and Confluent-framed payloads
  • adds dispatcher tests for routing, targeted delivery, filter mutation, and client health policy
  • adds in-process bootstrap tests for gRPC subscription setup with hand-written fakes
  • adds App::run() orchestration tests with recording sinks and scripted sources
  • adds a final pass for output formatting helpers

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for core services and data handling across snapshot retrieval, account updates, and subscription validation.
    • Added comprehensive integration tests for event ordering, error handling, and client health tracking.
  • Refactor

    • Improved internal architecture to enhance testability and flexibility of service dependencies.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@thlorenz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 30 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 44 minutes and 30 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b570eb98-5441-45e5-b769-44d2f8d8944b

📥 Commits

Reviewing files that changed from the base of the PR and between 69a4683 and 9e0986b.

📒 Files selected for processing (17)
  • geyser-plugin/src/account_update_publisher.rs
  • geyser-plugin/src/config.rs
  • geyser-plugin/src/confirmation_buffer.rs
  • geyser-plugin/src/initial_account_backfill/mod.rs
  • geyser-plugin/src/ksql.rs
  • geyser-plugin/src/plugin/mod.rs
  • geyser-plugin/src/publisher.rs
  • geyser-plugin/src/server/accounts.rs
  • geyser-plugin/src/server/subscriptions.rs
  • grpc-service/src/app.rs
  • grpc-service/src/domain.rs
  • grpc-service/src/grpc_service/convert.rs
  • grpc-service/src/grpc_service/dispatcher.rs
  • grpc-service/src/grpc_service/service.rs
  • grpc-service/src/kafka.rs
  • grpc-service/src/ksql.rs
  • grpc-service/src/output.rs

Walkthrough

This pull request introduces a dependency injection pattern by adding trait abstractions (SnapshotStore, AccountUpdateSource, ValidatorSubscriptions) and refactoring core components (App, GrpcSubscriptionService) to accept these traits as generic parameters instead of concrete dependencies. Concrete implementations for Kafka and KSQL are updated to implement the new traits. Extensive test coverage is added across all modified files.

Changes

Cohort / File(s) Summary
Trait Infrastructure
src/traits.rs
Added Send + Sync bounds to existing AccountSink and StatusSink traits. Introduced three new trait abstractions: SnapshotStore (async methods for fetching filtered snapshots and individual accounts), ValidatorSubscriptions (whitelist management), and AccountUpdateSource (stream processing with optional filtering).
Core Application Refactoring
src/app.rs, src/grpc_service/runtime.rs
App refactored from concrete fields to generic trait-bound parameters <P: SnapshotStore, K: AccountUpdateSource, A: AccountSink, S: StatusSink>. Constructor methods instantiate concrete implementations and delegate to public build method. Updated runtime binding names (snapshot_store, validator_subscriptions) to reflect abstraction layer. Added comprehensive test module validating event ordering, error propagation, and empty snapshot handling.
External Data Source Implementations
src/kafka.rs, src/ksql.rs
KafkaAccountUpdateStream implements AccountUpdateSource trait. KsqlAccountSnapshotClient implements SnapshotStore trait. Both additions include extensive test coverage: KSQL tests cover JSON parsing, field decoding, and error cases; Kafka tests cover Confluent framing stripping and protobuf decoding.
gRPC Service Refactoring
src/grpc_service/service.rs, src/grpc_service/init_subs.rs
GrpcSubscriptionService refactored to generic <P: SnapshotStore, V: ValidatorSubscriptions> parameters. InitSubsClient implements ValidatorSubscriptions trait. Service trait bounds propagated through Geyser impl and bootstrap logic. Added test module with fake implementations and filter/bootstrap behavior validation.
Test Coverage Extensions
src/domain.rs, src/grpc_service/convert.rs, src/grpc_service/dispatcher.rs, src/output.rs
Added comprehensive test modules: domain tests cover pubkey filter parsing, account state matching, and base58 encoding; conversion tests validate to_subscribe_update snapshot/live transformations and base58 error handling; dispatcher tests cover client delivery, filtering, health evaluation, and failure policies; output tests validate identifier, snapshot, and update formatting.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly matches the primary objective: expanding unit test coverage for the gRPC service with comprehensive tests across multiple components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
grpc-service/src/ksql.rs (1)

217-224: ⚠️ Potential issue | 🟠 Major

Reject negative JSON integers before casting to u64.

The new integer tests cover non-negative i64, but parse_u64_field(json!(-1)) currently returns u64::MAX via number as u64. That can silently corrupt KSQL numeric fields.

🐛 Proposed parser and coverage fix
 fn parse_u64_field(value: &Value) -> GeykagResult<u64> {
     if let Some(number) = value.as_u64() {
         return Ok(number);
     }
 
     if let Some(number) = value.as_i64() {
-        return Ok(number as u64);
+        if number >= 0 {
+            return Ok(number as u64);
+        }
     }
 
     Err(GeykagError::InvalidJsonInteger {
         value: value.clone(),
     })
 }
 #[test]
 fn parse_u64_field_accepts_non_negative_i64_values() {
     assert_eq!(parse_u64_field(&json!(42_i64)).unwrap(), 42);
 }
 
+#[test]
+fn parse_u64_field_rejects_negative_i64_values() {
+    let error = parse_u64_field(&json!(-1_i64)).unwrap_err();
+
+    assert!(matches!(error, GeykagError::InvalidJsonInteger { .. }));
+}
+
 #[test]
 fn parse_u64_field_rejects_string_values() {
     let error = parse_u64_field(&json!("42")).unwrap_err();

Also applies to: 617-627

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc-service/src/ksql.rs` around lines 217 - 224, The parser parse_u64_field
currently casts negative i64 values to u64 (producing u64::MAX) which silently
corrupts fields; update parse_u64_field to reject negative integers by checking
the i64 sign before casting and return a GeykagResult error (with a clear
message like "negative value for unsigned field") when number < 0; apply the
same fix to the duplicate/related logic at the other occurrence referenced
(lines 617-627) so all i64-to-u64 conversions validate non-negativity first and
only cast safe, non-negative values.
grpc-service/src/grpc_service/service.rs (2)

311-328: ⚠️ Potential issue | 🟠 Major

Bootstrap the initial subscription filter, not only later patches.

Line 311 parses the first request and Line 320 registers those pubkeys with the dispatcher, but bootstrap_new_pubkeys_impl is only invoked for subsequent stream messages at Lines 365-372. A client that only sends the required initial subscribe request will not receive existing snapshots, and ksql-missing pubkeys will never be whitelisted with the validator until the client sends a later filter update.

🐛 Proposed fix to bootstrap the initial filter in the reader task
         let initial_filter = parse_accounts_filter(&first_req)?;
+        let initial_filter_for_bootstrap = initial_filter.clone();
         info!(
             filter_size = initial_filter.len(),
             "new gRPC subscriber connected"
         );
@@
         let dispatcher = self.dispatcher.clone();
         let snapshot_store = self.snapshot_store.clone();
         let validator_subscriptions = self.validator_subscriptions.clone();
         tokio::spawn(async move {
+            bootstrap_new_pubkeys_impl(
+                &dispatcher,
+                &snapshot_store,
+                &validator_subscriptions,
+                client_id,
+                initial_filter_for_bootstrap,
+            )
+            .await;
+
             while let Some(result) = request_stream.next().await {
                 match result {

Also applies to: 363-372

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc-service/src/grpc_service/service.rs` around lines 311 - 328, The initial
parsed filter from parse_accounts_filter(...) is registered via
self.dispatcher.add_client(...) but never passed into bootstrap_new_pubkeys_impl
for the spawned reader task, so clients that only send the initial subscribe
request miss existing snapshots and validator whitelisting; to fix, inside the
tokio::spawn block (the reader task created after add_client), invoke
bootstrap_new_pubkeys_impl(client_id, initial_filter.clone(),
snapshot_store.clone(), validator_subscriptions.clone(), dispatcher.clone()) (or
the appropriate signature) before entering the message-read loop so the
initial_filter is treated the same as subsequent updates and triggers snapshot
delivery and ksql whitelisting; keep use of the same client_id, snapshot_store,
validator_subscriptions and dispatcher variables already captured for the task.

141-180: ⚠️ Potential issue | 🟠 Major

Do not whitelist pubkeys after aborting bootstrap for a gone client.

The break branches at Lines 149, 157, 165, and 174 still fall through to Lines 179-191. If an earlier pubkey was missing from ksql and queued in pubkeys_to_whitelist, a later ClientNotFound/policy removal/send error will still whitelist validator pubkeys for a client that can no longer receive them.

🐛 Proposed fix to stop before validator whitelisting on terminal delivery failures
             Ok(TargetedSendResult::ClientNotFound) => {
                 warn!(
                     client_id,
                     pubkey = %pubkey_b58,
                     "targeted snapshot skipped because client is no longer registered"
                 );
-                break;
+                return;
             }
             Ok(TargetedSendResult::FailedButRetained) => {
                 warn!(
                     client_id,
                     pubkey = %pubkey_b58,
                     "targeted snapshot delivery failed but client was retained"
                 );
-                break;
+                return;
             }
             Ok(TargetedSendResult::RemovedByPolicy) => {
                 info!(
                     client_id,
                     pubkey = %pubkey_b58,
                     "targeted snapshot delivery removed client by dispatcher policy"
                 );
-                break;
+                return;
             }
             Err(error) => {
                 warn!(
                     client_id,
                     pubkey = %pubkey_b58,
                     error = %error,
                     "failed to queue targeted snapshot update"
                 );
-                break;
+                return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@grpc-service/src/grpc_service/service.rs` around lines 141 - 180, The match's
break branches only exit the inner loop but still fall through to whitelisting;
record a terminal-failure flag (e.g., let mut aborted_delivery = false) and set
aborted_delivery = true inside the ClientNotFound, FailedButRetained,
RemovedByPolicy and Err(error) arms (where you currently break), then break;
after the loop check if aborted_delivery { return; } to avoid executing the
subsequent pubkeys_to_whitelist logic (references: pubkeys_to_whitelist,
dispatcher.send_to_client, TargetedSendResult::ClientNotFound /
FailedButRetained / RemovedByPolicy).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@grpc-service/src/grpc_service/dispatcher.rs`:
- Around line 769-785: The test currently relies on tokio::task::yield_now() to
wait for the dispatcher to process the enqueued add_client/remove_client
command, which can race; instead make the test perform an acknowledged
dispatcher operation so you know the client registration has been applied before
publishing. Replace the yield_now() usage in tests (e.g.,
add_client_registers_client_and_receives_matching_updates and the similar test
at 836-853) with an awaited acknowledged command such as calling
dispatcher.update_filter(...) or another dispatcher API that returns only after
the command is applied; ensure you await that acknowledgement immediately after
dispatcher.add_client(...) (and before dispatcher.try_publish(...)) so the
client registration is deterministic.

---

Outside diff comments:
In `@grpc-service/src/grpc_service/service.rs`:
- Around line 311-328: The initial parsed filter from parse_accounts_filter(...)
is registered via self.dispatcher.add_client(...) but never passed into
bootstrap_new_pubkeys_impl for the spawned reader task, so clients that only
send the initial subscribe request miss existing snapshots and validator
whitelisting; to fix, inside the tokio::spawn block (the reader task created
after add_client), invoke bootstrap_new_pubkeys_impl(client_id,
initial_filter.clone(), snapshot_store.clone(), validator_subscriptions.clone(),
dispatcher.clone()) (or the appropriate signature) before entering the
message-read loop so the initial_filter is treated the same as subsequent
updates and triggers snapshot delivery and ksql whitelisting; keep use of the
same client_id, snapshot_store, validator_subscriptions and dispatcher variables
already captured for the task.
- Around line 141-180: The match's break branches only exit the inner loop but
still fall through to whitelisting; record a terminal-failure flag (e.g., let
mut aborted_delivery = false) and set aborted_delivery = true inside the
ClientNotFound, FailedButRetained, RemovedByPolicy and Err(error) arms (where
you currently break), then break; after the loop check if aborted_delivery {
return; } to avoid executing the subsequent pubkeys_to_whitelist logic
(references: pubkeys_to_whitelist, dispatcher.send_to_client,
TargetedSendResult::ClientNotFound / FailedButRetained / RemovedByPolicy).

In `@grpc-service/src/ksql.rs`:
- Around line 217-224: The parser parse_u64_field currently casts negative i64
values to u64 (producing u64::MAX) which silently corrupts fields; update
parse_u64_field to reject negative integers by checking the i64 sign before
casting and return a GeykagResult error (with a clear message like "negative
value for unsigned field") when number < 0; apply the same fix to the
duplicate/related logic at the other occurrence referenced (lines 617-627) so
all i64-to-u64 conversions validate non-negativity first and only cast safe,
non-negative values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d110daa7-b640-4281-938c-146f95f2a98c

📥 Commits

Reviewing files that changed from the base of the PR and between fe1b1df and 69a4683.

📒 Files selected for processing (11)
  • grpc-service/src/app.rs
  • grpc-service/src/domain.rs
  • grpc-service/src/grpc_service/convert.rs
  • grpc-service/src/grpc_service/dispatcher.rs
  • grpc-service/src/grpc_service/init_subs.rs
  • grpc-service/src/grpc_service/runtime.rs
  • grpc-service/src/grpc_service/service.rs
  • grpc-service/src/kafka.rs
  • grpc-service/src/ksql.rs
  • grpc-service/src/output.rs
  • grpc-service/src/traits.rs

Comment thread grpc-service/src/grpc_service/dispatcher.rs
@thlorenz thlorenz merged commit 98f2aa6 into master Apr 22, 2026
1 check passed
@thlorenz thlorenz deleted the tests branch April 22, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant