package controller import ( "bytes" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" ) type expenseServiceStub struct { getAllCalls []validation.Query } var _ service.ExpenseService = (*expenseServiceStub)(nil) func (s *expenseServiceStub) GetAll(_ *fiber.Ctx, params *validation.Query) ([]dto.ExpenseListDTO, int64, error) { callCopy := *params s.getAllCalls = append(s.getAllCalls, callCopy) switch params.Page { case 1: return []dto.ExpenseListDTO{ buildExpenseListForControllerTest("EXP-00001"), buildExpenseListForControllerTest("EXP-00002"), }, 3, nil case 2: return []dto.ExpenseListDTO{ buildExpenseListForControllerTest("EXP-00003"), }, 3, nil default: return []dto.ExpenseListDTO{}, 3, nil } } func (s *expenseServiceStub) GetOne(_ *fiber.Ctx, _ uint) (*dto.ExpenseDetailDTO, error) { return &dto.ExpenseDetailDTO{}, nil } func (s *expenseServiceStub) CreateOne(_ *fiber.Ctx, _ *validation.Create) (*dto.ExpenseDetailDTO, error) { return &dto.ExpenseDetailDTO{}, nil } func (s *expenseServiceStub) UpdateOne(_ *fiber.Ctx, _ *validation.Update, _ uint) (*dto.ExpenseDetailDTO, error) { return &dto.ExpenseDetailDTO{}, nil } func (s *expenseServiceStub) DeleteOne(_ *fiber.Ctx, _ uint64) error { return nil } func (s *expenseServiceStub) CreateRealization(_ *fiber.Ctx, _ uint, _ *validation.CreateRealization) (*dto.ExpenseDetailDTO, error) { return &dto.ExpenseDetailDTO{}, nil } func (s *expenseServiceStub) CompleteExpense(_ *fiber.Ctx, _ uint, _ *string) (*dto.ExpenseDetailDTO, error) { return &dto.ExpenseDetailDTO{}, nil } func (s *expenseServiceStub) UpdateRealization(_ *fiber.Ctx, _ uint, _ *validation.UpdateRealization) (*dto.ExpenseDetailDTO, error) { return &dto.ExpenseDetailDTO{}, nil } func (s *expenseServiceStub) DeleteDocument(_ *fiber.Ctx, _ uint, _ uint64, _ bool) error { return nil } func (s *expenseServiceStub) Approval(_ *fiber.Ctx, _ *validation.ApprovalRequest, _ string) ([]dto.ExpenseDetailDTO, error) { return nil, nil } func (s *expenseServiceStub) BulkApproveToStatus(_ *fiber.Ctx, _ *validation.BulkApprovalRequest, _ approvalutils.ApprovalStep) ([]dto.ExpenseDetailDTO, error) { return nil, nil } func (s *expenseServiceStub) GetProgressRows(_ *fiber.Ctx, _ *exportprogress.Query) ([]exportprogress.Row, error) { return nil, nil } func TestExpenseControllerGetAllExportAllIgnoresRequestLimit(t *testing.T) { app := fiber.New() stub := &expenseServiceStub{} ctrl := NewExpenseController(stub) app.Get("/expenses", ctrl.GetAll) req := httptest.NewRequest( http.MethodGet, "/expenses?export=excel&type=all&page=9&limit=1&search=operasional", 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 !strings.Contains(contentType, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { t.Fatalf("unexpected content-type: %s", contentType) } disposition := resp.Header.Get("Content-Disposition") if !strings.Contains(disposition, "expenses_all_") { t.Fatalf("unexpected content-disposition: %s", disposition) } if len(stub.getAllCalls) != 2 { t.Fatalf("expected 2 GetAll calls, got %d", len(stub.getAllCalls)) } firstCall := stub.getAllCalls[0] secondCall := stub.getAllCalls[1] if firstCall.Page != 1 || secondCall.Page != 2 { t.Fatalf("expected internal paging page 1 and 2, got %d and %d", firstCall.Page, secondCall.Page) } if firstCall.Limit != expenseExcelExportFetchLimit || secondCall.Limit != expenseExcelExportFetchLimit { t.Fatalf("expected internal limit %d, got %d and %d", expenseExcelExportFetchLimit, firstCall.Limit, secondCall.Limit) } if firstCall.Search != "operasional" { t.Fatalf("expected search filter to be forwarded, got %q", firstCall.Search) } 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() if got, _ := file.GetCellValue(expenseExportSheetName, "A1"); got != "No" { t.Fatalf("expected A1 header to be No, got %q", got) } if got, _ := file.GetCellValue(expenseExportSheetName, "C2"); got != "EXP-00001" { t.Fatalf("expected first row reference EXP-00001, got %q", got) } } func TestExpenseControllerGetAllKeepsPaginationValidationForNonExportAll(t *testing.T) { app := fiber.New() stub := &expenseServiceStub{} ctrl := NewExpenseController(stub) app.Get("/expenses", ctrl.GetAll) req := httptest.NewRequest(http.MethodGet, "/expenses?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 TestExpenseControllerGetAllProgressExportUnchanged(t *testing.T) { app := fiber.New() stub := &expenseServiceStub{} ctrl := NewExpenseController(stub) app.Get("/expenses", ctrl.GetAll) req := httptest.NewRequest( http.MethodGet, "/expenses?export=excel&type=progress&start_date=2026-04-01&end_date=2026-04-22&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.StatusOK { t.Fatalf("expected status 200, got %d", resp.StatusCode) } if len(stub.getAllCalls) != 0 { t.Fatalf("expected list GetAll not to be called for progress export, got %d calls", len(stub.getAllCalls)) } } func buildExpenseListForControllerTest(referenceNumber string) dto.ExpenseListDTO { approvedAction := string(entity.ApprovalActionApproved) return dto.ExpenseListDTO{ ExpenseBaseDTO: dto.ExpenseBaseDTO{ ReferenceNumber: referenceNumber, PoNumber: "PO-" + strings.TrimPrefix(referenceNumber, "EXP-"), TransactionDate: time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC), Category: "BOP", Supplier: &supplierDTO.SupplierRelationDTO{ Name: "Supplier A", }, Location: &locationDTO.LocationRelationDTO{ Name: "Farm A", }, }, GrandTotal: 1500000, LatestApproval: &approvalDTO.ApprovalRelationDTO{ StepName: "Finance", Action: &approvedAction, }, } }