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" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" ) type purchaseServiceStub struct { getAllCalls []validation.Query } var _ service.PurchaseService = (*purchaseServiceStub)(nil) func (s *purchaseServiceStub) GetAll(_ *fiber.Ctx, params *validation.Query) ([]entity.Purchase, int64, error) { callCopy := *params s.getAllCalls = append(s.getAllCalls, callCopy) switch params.Page { case 1: return []entity.Purchase{ buildPurchaseForControllerTest(1, "PR-00001"), buildPurchaseForControllerTest(2, "PR-00002"), }, 3, nil case 2: return []entity.Purchase{ buildPurchaseForControllerTest(3, "PR-00003"), }, 3, nil default: return []entity.Purchase{}, 3, nil } } func (s *purchaseServiceStub) GetOne(_ *fiber.Ctx, _ uint) (*entity.Purchase, error) { return &entity.Purchase{}, nil } func (s *purchaseServiceStub) CreateOne(_ *fiber.Ctx, _ *validation.CreatePurchaseRequest) (*entity.Purchase, error) { return &entity.Purchase{}, nil } func (s *purchaseServiceStub) ApproveStaffPurchase(_ *fiber.Ctx, _ uint, _ *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) { return &entity.Purchase{}, nil } func (s *purchaseServiceStub) ApproveManagerPurchase(_ *fiber.Ctx, _ uint, _ *validation.ApproveManagerPurchaseRequest) (*entity.Purchase, error) { return &entity.Purchase{}, nil } func (s *purchaseServiceStub) ReceiveProducts(_ *fiber.Ctx, _ uint, _ *validation.ReceivePurchaseRequest) (*entity.Purchase, error) { return &entity.Purchase{}, nil } func (s *purchaseServiceStub) DeleteItems(_ *fiber.Ctx, _ uint, _ *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) { return &entity.Purchase{}, nil } func (s *purchaseServiceStub) DeletePurchase(_ *fiber.Ctx, _ uint) error { return nil } func (s *purchaseServiceStub) GetProgressRows(_ *fiber.Ctx, _ *exportprogress.Query) ([]exportprogress.Row, error) { return nil, nil } func TestPurchaseControllerGetAllExportAllIgnoresRequestLimit(t *testing.T) { app := fiber.New() stub := &purchaseServiceStub{} ctrl := NewPurchaseController(stub) app.Get("/purchases", ctrl.GetAll) req := httptest.NewRequest( http.MethodGet, "/purchases?export=excel&type=all&page=9&limit=1&search=po&supplier_id=7&area_id=4&location_id=2&product_category_id=1,2&approval_status=pending&po_date_from=2026-01-01&po_date_to=2026-01-31&created_from=2026-02-01&created_to=2026-02-20", 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, "purchases_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 != purchaseExcelExportFetchLimit || secondCall.Limit != purchaseExcelExportFetchLimit { t.Fatalf("expected internal limit %d, got %d and %d", purchaseExcelExportFetchLimit, firstCall.Limit, secondCall.Limit) } if firstCall.Search != "po" || firstCall.SupplierID != 7 || firstCall.AreaID != 4 || firstCall.LocationID != 2 || firstCall.ProductCategoryID != "1,2" || firstCall.ApprovalStatus != "pending" || firstCall.PoDateFrom != "2026-01-01" || firstCall.PoDateTo != "2026-01-31" || firstCall.CreatedFrom != "2026-02-01" || firstCall.CreatedTo != "2026-02-20" { 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() if got, _ := file.GetCellValue(purchaseExportSheetName, "A1"); got != "PR Number" { t.Fatalf("expected A1 header to be PR Number, got %q", got) } if got, _ := file.GetCellValue(purchaseExportSheetName, "A2"); got != "PR-00001" { t.Fatalf("expected first row PR-00001, got %q", got) } } func TestPurchaseControllerGetAllKeepsPaginationValidationForNonExportAll(t *testing.T) { app := fiber.New() stub := &purchaseServiceStub{} ctrl := NewPurchaseController(stub) app.Get("/purchases", ctrl.GetAll) req := httptest.NewRequest(http.MethodGet, "/purchases?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 buildPurchaseForControllerTest(id uint, prNumber string) entity.Purchase { poNumber := "PO-" + strings.TrimPrefix(prNumber, "PR-") poDate := time.Date(2026, time.April, 22, 0, 0, 0, 0, time.UTC) notes := "catatan" approvalAction := entity.ApprovalActionApproved return entity.Purchase{ Id: id, PrNumber: prNumber, PoNumber: &poNumber, PoDate: &poDate, Notes: ¬es, Supplier: entity.Supplier{ Id: 10, Name: "Supplier A", }, LatestApproval: &entity.Approval{ Id: 1, StepName: "Manager Purchase", Action: &approvalAction, }, Items: []entity.PurchaseItem{ { Id: id*10 + 1, TotalPrice: 1000000, Product: &entity.Product{ Id: id*100 + 1, Name: "Pakan Starter", }, }, }, } }