package main import ( "context" "errors" "strings" "testing" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" transferSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" ) // ── Fake executor ───────────────────────────────────────────────────────────── type fakeSystemTransferExecutor struct { createRequests []*transferSvc.SystemTransferRequest createResponses []*entity.StockTransfer createErrors []error deletedTransferIDs []uint deleteErrors map[uint]error } func (f *fakeSystemTransferExecutor) CreateSystemTransfer(_ context.Context, req *transferSvc.SystemTransferRequest) (*entity.StockTransfer, error) { f.createRequests = append(f.createRequests, req) idx := len(f.createRequests) - 1 if idx < len(f.createErrors) && f.createErrors[idx] != nil { return nil, f.createErrors[idx] } if idx < len(f.createResponses) && f.createResponses[idx] != nil { return f.createResponses[idx], nil } return &entity.StockTransfer{Id: uint64(1000 + idx), MovementNumber: "PND-LTI-DEFAULT"}, nil } func (f *fakeSystemTransferExecutor) DeleteSystemTransfer(_ context.Context, id uint, _ uint) error { f.deletedTransferIDs = append(f.deletedTransferIDs, id) if f.deleteErrors != nil { return f.deleteErrors[id] } return nil } // ── applyFarmWarehouseOverride ──────────────────────────────────────────────── func TestApplyOverrideResolvesMultiFarmLocation(t *testing.T) { farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{ {ID: 50, Name: "Farm A"}, {ID: 51, Name: "Farm B"}, }, }, } if err := applyFarmWarehouseOverride(farmMap, 51); err != nil { t.Fatalf("unexpected error: %v", err) } info := farmMap[10] if info.ChosenID != 51 { t.Errorf("expected ChosenID=51, got %d", info.ChosenID) } if info.ChosenName != "Farm B" { t.Errorf("expected ChosenName=Farm B, got %s", info.ChosenName) } if len(info.OtherFarm) != 1 || info.OtherFarm[0].ID != 50 { t.Errorf("expected OtherFarm=[Farm A], got %+v", info.OtherFarm) } } func TestApplyOverrideErrorsWhenIDNotInAllFarm(t *testing.T) { farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{ {ID: 50, Name: "Farm A"}, {ID: 51, Name: "Farm B"}, }, }, } err := applyFarmWarehouseOverride(farmMap, 99) if err == nil { t.Fatal("expected error for unknown warehouse id, got nil") } if !strings.Contains(err.Error(), "99") { t.Errorf("error should mention the invalid id, got: %v", err) } if !strings.Contains(err.Error(), "Farm A") || !strings.Contains(err.Error(), "Farm B") { t.Errorf("error should list available warehouses, got: %v", err) } } func TestApplyOverrideIgnoresSingleFarmLocations(t *testing.T) { farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Jamali", AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50, ChosenName: "Farm A", }, } // Override ID 50 is present, but there is only 1 farm; the function should // not touch this location (no OtherFarm to populate). if err := applyFarmWarehouseOverride(farmMap, 50); err != nil { t.Fatalf("unexpected error: %v", err) } if len(farmMap[10].OtherFarm) != 0 { t.Errorf("expected no OtherFarm for single-farm location, got %+v", farmMap[10].OtherFarm) } } func TestApplyOverrideNoopWhenZero(t *testing.T) { farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{ {ID: 50, Name: "Farm A"}, {ID: 51, Name: "Farm B"}, }, }, } if err := applyFarmWarehouseOverride(farmMap, 0); err != nil { t.Fatalf("unexpected error: %v", err) } if farmMap[10].ChosenID != 0 { t.Errorf("expected ChosenID unchanged (0), got %d", farmMap[10].ChosenID) } } // ── listUnresolvedLocations ─────────────────────────────────────────────────── func TestListUnresolvedLocationsReturnsOnlyAmbiguous(t *testing.T) { farmMap := map[uint]farmWarehouseInfo{ 1: {LocationID: 1, LocationName: "Jamali", AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50}, 2: { LocationID: 2, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{{ID: 60, Name: "Farm X"}, {ID: 61, Name: "Farm Y"}}, // ChosenID = 0: unresolved }, 3: {LocationID: 3, LocationName: "Tamansari", AllFarm: nil}, // no farm at all, not an error here } msgs := listUnresolvedLocations(farmMap) if len(msgs) != 1 { t.Fatalf("expected 1 unresolved message, got %d: %v", len(msgs), msgs) } if !strings.Contains(msgs[0], "Cijangkar") { t.Errorf("message should name the ambiguous location, got: %s", msgs[0]) } if !strings.Contains(msgs[0], "Farm X") || !strings.Contains(msgs[0], "Farm Y") { t.Errorf("message should list available warehouses, got: %s", msgs[0]) } } // ── buildTransferPlan — kandang source ─────────────────────────────────────── func TestBuildPlanKandangEligibleGroupedByWarehousePair(t *testing.T) { opts := &commandOptions{RunID: "test-run"} farmMap := map[uint]farmWarehouseInfo{ 10: {LocationID: 10, LocationName: "Jamali", AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50, ChosenName: "Farm A"}, } stocks := []kandangStockRow{ {LocationID: 10, LocationName: "Jamali", SourceWarehouseID: 20, SourceWarehouseName: "K1", ProductID: 1, ProductName: "Pakan A", OnHandQty: 100, LeftoverQty: 100, SourceType: sourceTypeKandang}, {LocationID: 10, LocationName: "Jamali", SourceWarehouseID: 20, SourceWarehouseName: "K1", ProductID: 2, ProductName: "OVK B", OnHandQty: 50, AllocatedQty: 10, LeftoverQty: 40, SourceType: sourceTypeKandang}, } reportRows, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 1 || len(groups[0].Rows) != 2 { t.Fatalf("expected 1 group with 2 rows, got %d groups", len(groups)) } if groups[0].SourceType != sourceTypeKandang { t.Errorf("expected group source type kandang_to_farm, got %s", groups[0].SourceType) } for _, row := range reportRows { if row.Status != "eligible" { t.Errorf("expected eligible, got %s for %s", row.Status, row.ProductName) } } if reportRows[1].Qty != 40 { t.Errorf("expected leftover qty 40 for OVK B, got %.3f", reportRows[1].Qty) } } func TestBuildPlanSkipsMissingFarmWarehouse(t *testing.T) { opts := &commandOptions{RunID: "test-run"} farmMap := map[uint]farmWarehouseInfo{ 10: {LocationID: 10, LocationName: "Jamali", AllFarm: nil}, } stocks := []kandangStockRow{ {LocationID: 10, SourceWarehouseID: 20, ProductID: 1, ProductName: "Pakan A", OnHandQty: 100, LeftoverQty: 100, SourceType: sourceTypeKandang}, } reportRows, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 0 { t.Fatalf("expected no groups, got %d", len(groups)) } if reportRows[0].Status != "skipped" || reportRows[0].Reason != "missing_farm_warehouse" { t.Errorf("unexpected: %s / %s", reportRows[0].Status, reportRows[0].Reason) } } func TestBuildPlanErrorForUnresolvedMultiFarmWarehouse(t *testing.T) { opts := &commandOptions{RunID: "test-run"} farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{{ID: 60, Name: "Farm X"}, {ID: 61, Name: "Farm Y"}}, // ChosenID = 0: unresolved }, } stocks := []kandangStockRow{ {LocationID: 10, LocationName: "Cijangkar", SourceWarehouseID: 21, ProductID: 3, ProductName: "Pakan C", OnHandQty: 200, LeftoverQty: 200, SourceType: sourceTypeKandang}, } reportRows, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 0 { t.Fatalf("expected no groups, got %d", len(groups)) } if reportRows[0].Status != "error" { t.Errorf("expected error status, got %s", reportRows[0].Status) } if !strings.Contains(reportRows[0].Reason, "multiple_farm_warehouses") { t.Errorf("reason should mention multiple_farm_warehouses, got: %s", reportRows[0].Reason) } // The error message must list the available warehouses so the operator knows // which --farm-warehouse-id to use. if !strings.Contains(reportRows[0].Reason, "Farm X") || !strings.Contains(reportRows[0].Reason, "Farm Y") { t.Errorf("reason should list available warehouses, got: %s", reportRows[0].Reason) } } func TestBuildPlanSkipsFullyAllocatedKandangStock(t *testing.T) { opts := &commandOptions{RunID: "test-run"} farmMap := map[uint]farmWarehouseInfo{ 10: {LocationID: 10, AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50, ChosenName: "Farm A"}, } stocks := []kandangStockRow{ {LocationID: 10, SourceWarehouseID: 20, ProductID: 1, ProductName: "Pakan A", OnHandQty: 100, AllocatedQty: 100, LeftoverQty: 0, SourceType: sourceTypeKandang}, {LocationID: 10, SourceWarehouseID: 20, ProductID: 2, ProductName: "OVK B", OnHandQty: 80, AllocatedQty: 30, LeftoverQty: 50, SourceType: sourceTypeKandang}, } _, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 1 || len(groups[0].Rows) != 1 { t.Fatalf("expected 1 group with 1 eligible row, got groups=%d", len(groups)) } if groups[0].Rows[0].ProductName != "OVK B" { t.Errorf("expected only OVK B to be eligible, got %s", groups[0].Rows[0].ProductName) } } func TestBuildPlanSkipAmbiguousDowngradesErrorToSkipped(t *testing.T) { opts := &commandOptions{RunID: "test-run", SkipAmbiguous: true} farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{{ID: 60, Name: "Farm X"}, {ID: 61, Name: "Farm Y"}}, // ChosenID = 0: unresolved }, 11: { LocationID: 11, LocationName: "Jamali", AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50, ChosenName: "Farm A", }, } stocks := []kandangStockRow{ {LocationID: 10, LocationName: "Cijangkar", SourceWarehouseID: 21, ProductID: 3, ProductName: "Pakan C", OnHandQty: 200, LeftoverQty: 200, SourceType: sourceTypeKandang}, {LocationID: 11, LocationName: "Jamali", SourceWarehouseID: 20, ProductID: 1, ProductName: "Pakan A", OnHandQty: 100, LeftoverQty: 100, SourceType: sourceTypeKandang}, } reportRows, groups := buildTransferPlan(opts, farmMap, stocks) // Ambiguous location must be skipped, not error. ambiguous := reportRows[0] if ambiguous.LocationName != "Cijangkar" { t.Fatalf("expected first row to be Cijangkar, got %s", ambiguous.LocationName) } if ambiguous.Status != "skipped" { t.Errorf("expected skipped with --skip-ambiguous, got %s", ambiguous.Status) } if !strings.Contains(ambiguous.Reason, "multiple_farm_warehouses") { t.Errorf("reason should still explain the cause, got: %s", ambiguous.Reason) } // Unambiguous location must still be eligible and grouped. if len(groups) != 1 || groups[0].LocationName != "Jamali" { t.Errorf("expected 1 group for Jamali, got %d groups", len(groups)) } } // ── applyFlagFilter (unit-level, via buildTransferPlan) ─────────────────────── // applyFlagFilter is a DB-level filter so we test its effect indirectly: the // flag filter is applied before rows reach buildTransferPlan, so we simulate // by only passing stock rows that the query would have returned. // The real guard is that loadKandangLeftoverStocks receives the filtered set. // Here we verify that buildTransferPlan itself is agnostic to the filter and // simply processes whatever rows it is given. func TestBuildPlanOnlyTransfersRowsPassedToIt(t *testing.T) { opts := &commandOptions{RunID: "test-run", FlagFilter: []string{"PAKAN"}} farmMap := map[uint]farmWarehouseInfo{ 10: {LocationID: 10, AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, ChosenID: 50, ChosenName: "Farm A"}, } // Simulate: only PAKAN products survived the DB filter; OVK was excluded. stocks := []kandangStockRow{ {LocationID: 10, SourceWarehouseID: 20, ProductID: 1, ProductName: "Pakan Broiler", OnHandQty: 100, LeftoverQty: 100, SourceType: sourceTypeKandang}, } _, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 1 || len(groups[0].Rows) != 1 { t.Fatalf("expected 1 group with 1 row, got %d groups", len(groups)) } if groups[0].Rows[0].ProductName != "Pakan Broiler" { t.Errorf("unexpected product: %s", groups[0].Rows[0].ProductName) } } // ── buildTransferPlan — farm_consolidation source ──────────────────────────── func TestBuildPlanFarmConsolidationCreatesOwnGroup(t *testing.T) { opts := &commandOptions{RunID: "test-run"} // Location 10 has 2 farm warehouses; Farm B (id=51) was chosen, Farm A // (id=50) is OtherFarm whose stocks need consolidating. farmMap := map[uint]farmWarehouseInfo{ 10: { LocationID: 10, LocationName: "Cijangkar", AllFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}, {ID: 51, Name: "Farm B"}}, ChosenID: 51, ChosenName: "Farm B", OtherFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}, }, } // Extra farm stock from Farm A + normal kandang stock. stocks := []kandangStockRow{ {LocationID: 10, LocationName: "Cijangkar", SourceWarehouseID: 20, SourceWarehouseName: "Kandang K1", ProductID: 1, ProductName: "Pakan A", OnHandQty: 100, LeftoverQty: 100, SourceType: sourceTypeKandang}, {LocationID: 10, LocationName: "Cijangkar", SourceWarehouseID: 50, SourceWarehouseName: "Farm A", ProductID: 2, ProductName: "OVK B", OnHandQty: 60, LeftoverQty: 60, SourceType: sourceTypeFarmConsol}, } reportRows, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 2 { t.Fatalf("expected 2 groups (one per source warehouse), got %d", len(groups)) } if len(reportRows) != 2 { t.Fatalf("expected 2 report rows, got %d", len(reportRows)) } groupsBySource := make(map[uint]*transferGroup, 2) for i := range groups { groupsBySource[groups[i].SourceWarehouseID] = &groups[i] } kandangGroup := groupsBySource[20] if kandangGroup == nil { t.Fatal("expected a group with SourceWarehouseID=20") } if kandangGroup.SourceType != sourceTypeKandang { t.Errorf("expected kandang_to_farm group, got %s", kandangGroup.SourceType) } if kandangGroup.FarmWarehouseID != 51 { t.Errorf("expected kandang group to target Farm B (51), got %d", kandangGroup.FarmWarehouseID) } consolGroup := groupsBySource[50] if consolGroup == nil { t.Fatal("expected a consolidation group with SourceWarehouseID=50") } if consolGroup.SourceType != sourceTypeFarmConsol { t.Errorf("expected farm_consolidation group, got %s", consolGroup.SourceType) } if consolGroup.FarmWarehouseID != 51 { t.Errorf("expected consolidation group to target Farm B (51), got %d", consolGroup.FarmWarehouseID) } } func TestBuildPlanFarmConsolidationSkipsZeroLeftover(t *testing.T) { opts := &commandOptions{RunID: "test-run"} farmMap := map[uint]farmWarehouseInfo{ 10: {LocationID: 10, AllFarm: []farmWarehouseEntry{{ID: 50}, {ID: 51}}, ChosenID: 51, ChosenName: "Farm B", OtherFarm: []farmWarehouseEntry{{ID: 50, Name: "Farm A"}}}, } stocks := []kandangStockRow{ {LocationID: 10, SourceWarehouseID: 50, ProductID: 1, ProductName: "Pakan A", OnHandQty: 100, AllocatedQty: 100, LeftoverQty: 0, SourceType: sourceTypeFarmConsol}, } reportRows, groups := buildTransferPlan(opts, farmMap, stocks) if len(groups) != 0 { t.Fatalf("expected no groups, got %d", len(groups)) } if reportRows[0].Status != "skipped" { t.Errorf("expected skipped, got %s", reportRows[0].Status) } } // ── executeApply ────────────────────────────────────────────────────────────── func TestExecuteApplyTagsReasonAndNotesWithSourceType(t *testing.T) { date := time.Date(2026, 4, 24, 0, 0, 0, 0, time.UTC) opts := &commandOptions{RunID: "run-apply", TransferDate: date, ActorID: 99} groups := []transferGroup{ { SourceType: sourceTypeKandang, LocationName: "Jamali", SourceWarehouseID: 20, SourceWarehouseName: "K1", FarmWarehouseID: 50, FarmWarehouseName: "Farm A", Rows: []*transferReportRow{{ProductID: 1, ProductName: "Pakan A", Qty: 100}}, }, { SourceType: sourceTypeFarmConsol, LocationName: "Cijangkar", SourceWarehouseID: 60, SourceWarehouseName: "Farm X", FarmWarehouseID: 61, FarmWarehouseName: "Farm Y", Rows: []*transferReportRow{{ProductID: 2, ProductName: "OVK B", Qty: 40}}, }, } executor := &fakeSystemTransferExecutor{} summary, err := executeApply(context.Background(), executor, opts, groups) if err != nil { t.Fatalf("unexpected error: %v", err) } if summary.GroupsApplied != 2 { t.Errorf("expected 2 groups applied, got %d", summary.GroupsApplied) } if len(executor.createRequests) != 2 { t.Fatalf("expected 2 create requests, got %d", len(executor.createRequests)) } // Both requests must carry the run_id in the reason for rollback to work. for i, req := range executor.createRequests { if !strings.Contains(req.TransferReason, "run_id=run-apply") { t.Errorf("request %d reason missing run_id: %s", i, req.TransferReason) } } // Notes for farm_consolidation should be distinct from kandang_to_farm. if !strings.Contains(executor.createRequests[0].StockLogNotes, "kandang to farm") { t.Errorf("kandang group notes should say 'kandang to farm', got: %s", executor.createRequests[0].StockLogNotes) } if !strings.Contains(executor.createRequests[1].StockLogNotes, "consolidation") { t.Errorf("consolidation group notes should say 'consolidation', got: %s", executor.createRequests[1].StockLogNotes) } } func TestExecuteApplyCreatesTransferWithCorrectProductsAndRecordsTransferID(t *testing.T) { date := time.Date(2026, 4, 24, 0, 0, 0, 0, time.UTC) opts := &commandOptions{RunID: "run-1", TransferDate: date, ActorID: 1} row1 := &transferReportRow{ProductID: 1, ProductName: "Pakan A", Qty: 100} row2 := &transferReportRow{ProductID: 2, ProductName: "OVK B", Qty: 40} groups := []transferGroup{ { SourceType: sourceTypeKandang, SourceWarehouseID: 20, FarmWarehouseID: 50, Rows: []*transferReportRow{row1, row2}, }, } executor := &fakeSystemTransferExecutor{ createResponses: []*entity.StockTransfer{{Id: 1001, MovementNumber: "PND-LTI-1001"}}, } _, err := executeApply(context.Background(), executor, opts, groups) if err != nil { t.Fatalf("unexpected error: %v", err) } if row1.Status != "applied" || row2.Status != "applied" { t.Errorf("both rows should be applied: %s, %s", row1.Status, row2.Status) } if row1.TransferID == nil || *row1.TransferID != 1001 { t.Errorf("expected transfer id 1001, got %+v", row1.TransferID) } if row1.MovementNumber == nil || *row1.MovementNumber != "PND-LTI-1001" { t.Errorf("expected movement number PND-LTI-1001, got %+v", row1.MovementNumber) } // Verify both products were included in the create request. if len(executor.createRequests[0].Products) != 2 { t.Errorf("expected 2 products in request, got %d", len(executor.createRequests[0].Products)) } } // ── executeRollback ─────────────────────────────────────────────────────────── func TestExecuteRollbackDeletesDescendingAndMarksStatuses(t *testing.T) { executor := &fakeSystemTransferExecutor{ deleteErrors: map[uint]error{200: errors.New("already consumed")}, } rows := []rollbackDetailRow{ {TransferID: 100, ProductName: "Pakan A"}, {TransferID: 200, ProductName: "OVK B"}, {TransferID: 100, ProductName: "Pakan C"}, } err := executeRollback(context.Background(), executor, rows, 99) if err == nil || !strings.Contains(err.Error(), "already consumed") { t.Fatalf("expected error for transfer 200, got: %v", err) } if executor.deletedTransferIDs[0] != 200 || executor.deletedTransferIDs[1] != 100 { t.Fatalf("expected delete order [200 100], got %v", executor.deletedTransferIDs) } if rows[0].Status != "rolled_back" || rows[2].Status != "rolled_back" { t.Errorf("transfer 100 rows must be rolled_back: %+v", rows) } if rows[1].Status != "failed" { t.Errorf("transfer 200 row must be failed: %+v", rows[1]) } } func TestExecuteRollbackRequiresActorID(t *testing.T) { err := executeRollback(context.Background(), &fakeSystemTransferExecutor{}, []rollbackDetailRow{{TransferID: 1}}, 0) if err == nil || !strings.Contains(err.Error(), "actor-id") { t.Fatalf("expected actor-id error, got: %v", err) } } // ── buildTransferReason / buildRunReasonMatcher ─────────────────────────────── func TestBuildTransferReasonMatchesRunReasonMatcher(t *testing.T) { runID := "product-farm-transfer-20260424T120000.000000000Z" date := time.Date(2026, 4, 24, 0, 0, 0, 0, time.UTC) reason := buildTransferReason(runID, "Jamali", "Gudang K1", "Gudang Farm Jamali", date) needle := strings.TrimSuffix(buildRunReasonMatcher(runID), "%") if !strings.HasPrefix(reason, needle) { t.Errorf("reason %q does not match matcher prefix %q", reason, needle) } } func TestBuildTransferReasonSanitizesPipes(t *testing.T) { reason := buildTransferReason("run-1", "Lok|asi", "Gudang|K1", "Farm|WH", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)) parts := strings.Split(reason, "|") // prefix + 5 key=value segments = 6 parts if len(parts) != 6 { t.Errorf("expected 6 pipe-separated segments, got %d: %v", len(parts), parts) } } func TestBuildStockLogNotesContainsSourceTypeHint(t *testing.T) { date := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) kandangNote := buildStockLogNotes("r", "Loc", "Src", "Dst", date, sourceTypeKandang) consolNote := buildStockLogNotes("r", "Loc", "Src", "Dst", date, sourceTypeFarmConsol) if !strings.Contains(kandangNote, "kandang to farm") { t.Errorf("kandang note should mention 'kandang to farm': %s", kandangNote) } if !strings.Contains(consolNote, "consolidation") { t.Errorf("consolidation note should mention 'consolidation': %s", consolNote) } } // ── summarizeReport ─────────────────────────────────────────────────────────── func TestSummarizeReportCountsAllStatuses(t *testing.T) { rows := []transferReportRow{ {Status: "eligible"}, {Status: "applied"}, {Status: "applied"}, {Status: "skipped"}, {Status: "error"}, {Status: "failed"}, } groups := []transferGroup{{}, {}} s := summarizeReport(rows, groups, 1) if s.RowsPlanned != 4 { // eligible + 2 applied + 1 failed t.Errorf("expected RowsPlanned=4, got %d", s.RowsPlanned) } if s.RowsApplied != 2 { t.Errorf("expected RowsApplied=2, got %d", s.RowsApplied) } if s.RowsSkipped != 1 { t.Errorf("expected RowsSkipped=1, got %d", s.RowsSkipped) } if s.RowsError != 1 { t.Errorf("expected RowsError=1, got %d", s.RowsError) } if s.RowsFailed != 1 { t.Errorf("expected RowsFailed=1, got %d", s.RowsFailed) } if s.GroupsPlanned != 2 || s.GroupsApplied != 1 { t.Errorf("unexpected group counts: %+v", s) } }