package controller import ( "bytes" "io" "net/http" "net/http/httptest" "reflect" "testing" "time" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" "gorm.io/gorm" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" ) type repportServiceStub struct { service.RepportService getExpenseCalls []validation.ExpenseQuery } func (s *repportServiceStub) GetExpense(_ *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { callCopy := *params callCopy.AllowedAreaIDs = append([]int64(nil), params.AllowedAreaIDs...) callCopy.AllowedLocationIDs = append([]int64(nil), params.AllowedLocationIDs...) s.getExpenseCalls = append(s.getExpenseCalls, callCopy) switch params.Page { case 1: return []dto.RepportExpenseListDTO{ buildExpenseListForControllerTest("REF-00001", "TRANSPORT 2"), buildExpenseListForControllerTest("REF-00002", "TRANSPORT"), }, 3, nil case 2: return []dto.RepportExpenseListDTO{ buildExpenseListForControllerTest("REF-00003", "TRANSPORT"), }, 3, nil default: return []dto.RepportExpenseListDTO{}, 3, nil } } func (s *repportServiceStub) DB() *gorm.DB { return nil } func TestRepportControllerGetExpenseExportAllIgnoresRequestLimit(t *testing.T) { app := fiber.New() stub := &repportServiceStub{} ctrl := NewRepportController(stub) app.Get("/reports/expense", ctrl.GetExpense) req := httptest.NewRequest( http.MethodGet, "/reports/expense?export=excel&type=all&page=9&limit=1&search=operasional&category=BOP&supplier_id=7&kandang_id=4&project_flock_kandang_id=2&nonstock_id=5&area_id=3&location_id=9&realization_date=2026-04-22", nil, ) resp, err := app.Test(req) if err != nil { t.Fatalf("unexpected app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected status 200, got %d", resp.StatusCode) } contentType := resp.Header.Get("Content-Type") if contentType != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" { t.Fatalf("unexpected content-type: %s", contentType) } disposition := resp.Header.Get("Content-Disposition") if !bytes.Contains([]byte(disposition), []byte("reports_expense_all_")) { t.Fatalf("unexpected content-disposition: %s", disposition) } if len(stub.getExpenseCalls) != 2 { t.Fatalf("expected 2 GetExpense calls, got %d", len(stub.getExpenseCalls)) } firstCall := stub.getExpenseCalls[0] secondCall := stub.getExpenseCalls[1] if firstCall.Page != 1 || secondCall.Page != 2 { t.Fatalf("expected internal pages 1 and 2, got %d and %d", firstCall.Page, secondCall.Page) } if firstCall.Limit != expenseReportExcelExportFetchLimit || secondCall.Limit != expenseReportExcelExportFetchLimit { t.Fatalf("expected internal limit %d, got %d and %d", expenseReportExcelExportFetchLimit, firstCall.Limit, secondCall.Limit) } if firstCall.Search != "operasional" || firstCall.Category != "BOP" || firstCall.SupplierId != 7 || firstCall.KandangId != 4 || firstCall.ProjectFlockKandangId != 2 || firstCall.NonstockId != 5 || firstCall.AreaId != 3 || firstCall.LocationId != 9 || firstCall.RealizationDate != "2026-04-22" { t.Fatalf("unexpected forwarded filters: %+v", firstCall) } payload, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read excel payload: %v", err) } file, err := excelize.OpenReader(bytes.NewReader(payload)) if err != nil { t.Fatalf("failed to parse excel payload: %v", err) } defer file.Close() sheetList := file.GetSheetList() expectedSheets := []string{"EKSPEDISI ADE", "EKSPEDISI LTI"} if !reflect.DeepEqual(sheetList, expectedSheets) { t.Fatalf("unexpected sheet list: got %v, expected %v", sheetList, expectedSheets) } if got, _ := file.GetCellValue("EKSPEDISI ADE", "A1"); got != "No" { t.Fatalf("expected EKSPEDISI ADE A1 to be No, got %q", got) } if got, _ := file.GetCellValue("EKSPEDISI ADE", "G2"); got != "TRANSPORT 2" { t.Fatalf("expected EKSPEDISI ADE G2 to be TRANSPORT 2, got %q", got) } if got, _ := file.GetCellValue("EKSPEDISI LTI", "G2"); got != "TRANSPORT" { t.Fatalf("expected EKSPEDISI LTI G2 to be TRANSPORT, got %q", got) } if got, _ := file.GetCellValue("EKSPEDISI LTI", "A4"); got != "Total" { t.Fatalf("expected EKSPEDISI LTI A4 to be Total, got %q", got) } } func TestRepportControllerGetExpenseKeepsPaginationValidationForNonExportAll(t *testing.T) { app := fiber.New() stub := &repportServiceStub{} ctrl := NewRepportController(stub) app.Get("/reports/expense", ctrl.GetExpense) req := httptest.NewRequest(http.MethodGet, "/reports/expense?page=1&limit=0", nil) resp, err := app.Test(req) if err != nil { t.Fatalf("unexpected app.Test error: %v", err) } defer resp.Body.Close() if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected status 400, got %d", resp.StatusCode) } } func buildExpenseListForControllerTest(reference, product string) dto.RepportExpenseListDTO { realizationDate := time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC) return dto.RepportExpenseListDTO{ RepportExpenseBaseDTO: dto.RepportExpenseBaseDTO{ ReferenceNumber: reference, PoNumber: "PO-001", Category: "BOP", Notes: "catatan expense", TransactionDate: time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC), RealizationDate: &realizationDate, Supplier: &supplierDTO.SupplierRelationDTO{ Name: "Supplier A", }, }, Kandang: &kandangDTO.KandangRelationDTO{ Name: "Kandang A", Location: &locationDTO.LocationRelationDTO{ Name: "Darawati", }, }, Pengajuan: dto.RepportExpensePengajuanDTO{ Qty: 1, Price: 50000, Notes: "catatan pengajuan", Nonstock: &nonstockDTO.NonstockRelationDTO{ Name: product, }, }, Realisasi: dto.RepportExpenseRealisasiDTO{ Qty: 1, Price: 50000, Notes: "catatan realisasi", Nonstock: &nonstockDTO.NonstockRelationDTO{ Name: product, }, }, TotalPengajuan: 50000, TotalRealisasi: 50000, LatestApproval: &approvalDTO.ApprovalRelationDTO{ StepName: "Realisasi", }, } }