Skip to content

Commit 2bb7fff

Browse files
FayedamzFayedamz
andauthored
test: cover disbursement worker status transitions (#861)
Co-authored-by: Fayedamz <2.95558173e+08+Fayedamz@users.noreply.github.com>
1 parent ac4d896 commit 2bb7fff

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

backend/src/services/disbursement_worker.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,10 +535,93 @@ async fn log_dispatch(
535535
Ok(())
536536
}
537537

538+
/// A compact representation of the batch state used to make status decisions.
539+
///
540+
/// The worker persists these same transitions in `claim_next_batch` and
541+
/// `finalize_batch`. Keeping the decision rule pure makes it possible to test
542+
/// the state machine without requiring a live PostgreSQL instance.
543+
#[derive(Debug, PartialEq, Eq)]
544+
struct BatchState {
545+
status: &'static str,
546+
pending_recipients: usize,
547+
failed_recipients: usize,
548+
total_recipients: usize,
549+
}
550+
551+
fn next_batch_status(state: &BatchState) -> &'static str {
552+
match state.status {
553+
"PENDING" => "PROCESSING",
554+
"PROCESSING" if state.pending_recipients > 0 => "PROCESSING",
555+
"PROCESSING" if state.failed_recipients == 0 => "COMPLETED",
556+
"PROCESSING" if state.failed_recipients == state.total_recipients => "FAILED",
557+
"PROCESSING" => "PARTIALLY_FAILED",
558+
status => status,
559+
}
560+
}
561+
538562
#[cfg(test)]
539563
mod tests {
540564
use super::*;
541565

566+
fn sample_batch(status: &'static str, pending: usize, failed: usize) -> BatchState {
567+
BatchState {
568+
status,
569+
pending_recipients: pending,
570+
failed_recipients: failed,
571+
total_recipients: 3,
572+
}
573+
}
574+
575+
#[test]
576+
fn worker_moves_pending_batch_to_processing() {
577+
let batch = sample_batch("PENDING", 3, 0);
578+
579+
assert_eq!(next_batch_status(&batch), "PROCESSING");
580+
}
581+
582+
#[test]
583+
fn worker_completes_processing_batch_when_all_recipients_are_submitted() {
584+
let batch = sample_batch("PROCESSING", 0, 0);
585+
586+
assert_eq!(next_batch_status(&batch), "COMPLETED");
587+
}
588+
589+
#[test]
590+
fn worker_recovers_after_retryable_error_and_completes_batch() {
591+
// A retryable SDP error leaves the recipient pending, so the batch must
592+
// remain PROCESSING and be eligible for another worker cycle.
593+
let after_error = sample_batch("PROCESSING", 1, 0);
594+
assert_eq!(next_batch_status(&after_error), "PROCESSING");
595+
596+
// Once the retry succeeds, no pending recipients remain and the batch
597+
// reaches its terminal completed state.
598+
let after_retry = sample_batch("PROCESSING", 0, 0);
599+
assert_eq!(next_batch_status(&after_retry), "COMPLETED");
600+
}
601+
602+
#[test]
603+
fn worker_marks_all_permanent_failures_as_failed() {
604+
let batch = sample_batch("PROCESSING", 0, 3);
605+
606+
assert_eq!(next_batch_status(&batch), "FAILED");
607+
}
608+
609+
#[test]
610+
fn worker_marks_mixed_success_and_failure_as_partially_failed() {
611+
let batch = sample_batch("PROCESSING", 0, 1);
612+
613+
assert_eq!(next_batch_status(&batch), "PARTIALLY_FAILED");
614+
}
615+
616+
#[test]
617+
fn terminal_batch_statuses_are_not_reopened() {
618+
let completed = sample_batch("COMPLETED", 0, 0);
619+
let failed = sample_batch("FAILED", 0, 3);
620+
621+
assert_eq!(next_batch_status(&completed), "COMPLETED");
622+
assert_eq!(next_batch_status(&failed), "FAILED");
623+
}
624+
542625
#[test]
543626
fn idempotency_key_is_stable_across_retries() {
544627
let id = Uuid::new_v4();

0 commit comments

Comments
 (0)