package test import ( "encoding/json" "errors" "fmt" "net/http" "strings" "testing" "github.com/gofiber/fiber/v2" "gorm.io/gorm" "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) func TestProductIntegration(t *testing.T) { app, db := setupIntegrationApp(t) uomID := createUom(t, app, "Kilogram") categoryID := createProductCategory(t, app, "Feed", "FED") sapSupplier1 := createSupplier(t, app, "Feed Supplier One", "fs1", string(utils.SupplierCategorySapronak)) sapSupplier2 := createSupplier(t, app, "Feed Supplier Two", "fs2", string(utils.SupplierCategorySapronak)) bopSupplier := createSupplier(t, app, "BOP Supplier", "bop1", string(utils.SupplierCategoryBOP)) productFlags := []string{string(utils.FlagDOC), string(utils.FlagPakan)} updatedProductFlags := []string{string(utils.FlagFinisher), string(utils.FlagObat)} t.Run("create product without suppliers succeeds with empty relations", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Supplierless Product", "brand": "Brand A", "uom_id": uomID, "product_category_id": categoryID, "product_price": 12000, }) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body)) } id := parseID(t, body) product := fetchProduct(t, db, id) if len(product.Suppliers) != 0 { t.Fatalf("expected no suppliers persisted, found %d", len(product.Suppliers)) } if len(product.Flags) != 0 { t.Fatalf("expected no flags persisted, found %d", len(product.Flags)) } resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", id), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode) } }) t.Run("create product with invalid uom fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed Invalid UOM", "brand": "Brand A", "uom_id": 9999, "product_category_id": categoryID, "product_price": 12000, "supplier_ids": []uint{sapSupplier1}, }) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when uom missing, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("create product with invalid category fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed Invalid Category", "brand": "Brand A", "uom_id": uomID, "product_category_id": 9999, "product_price": 12000, "supplier_ids": []uint{sapSupplier1}, }) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when category missing, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("create product with invalid supplier fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed Missing Supplier", "brand": "Brand A", "uom_id": uomID, "product_category_id": categoryID, "product_price": 12000, "supplier_ids": []uint{99999}, }) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when supplier missing, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("create product with BOP supplier fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed Wrong Supplier", "brand": "Brand A", "uom_id": uomID, "product_category_id": categoryID, "product_price": 12000, "supplier_ids": []uint{bopSupplier}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when supplier category invalid, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("create product with invalid flags fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed Invalid Flags", "brand": "Brand A", "uom_id": uomID, "product_category_id": categoryID, "product_price": 12000, "supplier_ids": []uint{sapSupplier1}, "flags": []string{"INVALID"}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body)) } }) var productID uint t.Run("create product succeeds", func(t *testing.T) { sku := "lfd-001" productID = createProduct(t, app, "Layer Feed", "Brand A", &sku, uomID, categoryID, 15000, []uint{sapSupplier1, sapSupplier2}, productFlags) product := fetchProduct(t, db, productID) if product.Name != "Layer Feed" { t.Fatalf("expected name Layer Feed, got %q", product.Name) } if product.Brand != "Brand A" { t.Fatalf("expected brand Brand A, got %q", product.Brand) } if product.Sku == nil || *product.Sku != strings.ToUpper(sku) { t.Fatalf("expected sku %q, got %+v", strings.ToUpper(sku), product.Sku) } if product.UomId != uomID { t.Fatalf("expected uom_id %d, got %d", uomID, product.UomId) } if product.ProductCategoryId != categoryID { t.Fatalf("expected product_category_id %d, got %d", categoryID, product.ProductCategoryId) } if product.CreatedBy != 1 { t.Fatalf("expected created_by 1, got %d", product.CreatedBy) } if len(product.Suppliers) != 2 { t.Fatalf("expected 2 suppliers, got %d", len(product.Suppliers)) } if len(product.Flags) != len(productFlags) { t.Fatalf("expected %d flags, got %d", len(productFlags), len(product.Flags)) } expectedFlags := make(map[string]struct{}, len(productFlags)) for _, flag := range productFlags { expectedFlags[strings.ToUpper(flag)] = struct{}{} } for _, flag := range product.Flags { if _, ok := expectedFlags[strings.ToUpper(flag.Name)]; !ok { t.Fatalf("unexpected flag %s present", flag.Name) } delete(expectedFlags, strings.ToUpper(flag.Name)) } if len(expectedFlags) != 0 { t.Fatalf("missing expected flags: %v", expectedFlags) } }) t.Run("creating duplicate name fails", func(t *testing.T) { sku := "lfd-002" resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed", "brand": "Brand B", "sku": sku, "uom_id": uomID, "product_category_id": categoryID, "product_price": 16000, "supplier_ids": []uint{sapSupplier1}, }) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected 409 when creating duplicate name, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("creating duplicate sku fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/products", map[string]any{ "name": "Layer Feed Premium", "brand": "Brand B", "sku": "LFD-001", "uom_id": uomID, "product_category_id": categoryID, "product_price": 17000, "supplier_ids": []uint{sapSupplier1}, }) if resp.StatusCode != fiber.StatusConflict { t.Fatalf("expected 409 when creating duplicate sku, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("get product detail returns nested data", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/products/%d", productID), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when fetching product, got %d: %s", resp.StatusCode, string(body)) } var payload struct { Data struct { Id uint `json:"id"` Name string `json:"name"` Brand string `json:"brand"` Uom *struct { Id uint `json:"id"` } `json:"uom"` Suppliers []map[string]any `json:"suppliers"` Flags []string `json:"flags"` } `json:"data"` } if err := json.Unmarshal(body, &payload); err != nil { t.Fatalf("failed to parse product detail: %v", err) } if payload.Data.Id != productID { t.Fatalf("expected id %d, got %d", productID, payload.Data.Id) } if payload.Data.Uom == nil || payload.Data.Uom.Id != uomID { t.Fatalf("expected uom id %d, got %+v", uomID, payload.Data.Uom) } if len(payload.Data.Suppliers) != 2 { t.Fatalf("expected 2 suppliers in response, got %d", len(payload.Data.Suppliers)) } if len(payload.Data.Flags) != len(productFlags) { t.Fatalf("expected %d flags in response, got %d", len(productFlags), len(payload.Data.Flags)) } expected := make(map[string]struct{}, len(productFlags)) for _, flag := range productFlags { expected[strings.ToUpper(flag)] = struct{}{} } for _, flag := range payload.Data.Flags { flag = strings.ToUpper(flag) if _, ok := expected[flag]; !ok { t.Fatalf("unexpected flag %s returned", flag) } delete(expected, flag) } if len(expected) != 0 { t.Fatalf("missing expected flags in response: %v", expected) } }) t.Run("update product with invalid supplier category fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ "supplier_ids": []uint{bopSupplier}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when updating with non SAPRONAK supplier, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("update product with invalid flags fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ "flags": []string{"UNKNOWN"}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when updating with invalid flags, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("update product succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ "name": "Layer Feed Updated", "brand": "Brand C", "sku": "lfd-100", "product_price": 18000, "selling_price": 19000, "tax": 1000, "expiry_period": 30, "supplier_ids": []uint{sapSupplier1}, "flags": updatedProductFlags, }) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when updating product, got %d: %s", resp.StatusCode, string(body)) } var payload struct { Data struct { Name string `json:"name"` Brand string `json:"brand"` Sku *string `json:"sku"` ProductPrice float64 `json:"product_price"` SupplierIds []map[string]any `json:"suppliers"` Flags []string `json:"flags"` } `json:"data"` } if err := json.Unmarshal(body, &payload); err != nil { t.Fatalf("failed to parse update response: %v", err) } if payload.Data.Name != "Layer Feed Updated" { t.Fatalf("expected updated name, got %q", payload.Data.Name) } if len(payload.Data.Flags) != len(updatedProductFlags) { t.Fatalf("expected %d flags in response, got %d", len(updatedProductFlags), len(payload.Data.Flags)) } respFlags := make(map[string]struct{}, len(payload.Data.Flags)) for _, flag := range payload.Data.Flags { respFlags[strings.ToUpper(flag)] = struct{}{} } for _, flag := range updatedProductFlags { if _, ok := respFlags[strings.ToUpper(flag)]; !ok { t.Fatalf("missing flag %s in response", flag) } } product := fetchProduct(t, db, productID) if product.Brand != "Brand C" { t.Fatalf("expected persisted brand Brand C, got %q", product.Brand) } if product.Sku == nil || *product.Sku != "LFD-100" { t.Fatalf("expected persisted sku LFD-100, got %+v", product.Sku) } if len(product.Suppliers) != 1 || product.Suppliers[0].Id != sapSupplier1 { t.Fatalf("expected supplier to be %d", sapSupplier1) } if len(product.Flags) != len(updatedProductFlags) { t.Fatalf("expected %d flags after update, got %d", len(updatedProductFlags), len(product.Flags)) } expectedFlags := make(map[string]struct{}, len(updatedProductFlags)) for _, flag := range updatedProductFlags { expectedFlags[strings.ToUpper(flag)] = struct{}{} } for _, flag := range product.Flags { upper := strings.ToUpper(flag.Name) if _, ok := expectedFlags[upper]; !ok { t.Fatalf("unexpected flag after update: %s", upper) } delete(expectedFlags, upper) } if len(expectedFlags) != 0 { t.Fatalf("missing flags after update: %v", expectedFlags) } }) t.Run("clear suppliers succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ "supplier_ids": []uint{}, }) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when clearing suppliers, got %d: %s", resp.StatusCode, string(body)) } product := fetchProduct(t, db, productID) if len(product.Suppliers) != 0 { t.Fatalf("expected no suppliers after clearing, got %d", len(product.Suppliers)) } if len(product.Flags) != len(updatedProductFlags) { t.Fatalf("expected flags untouched after clearing suppliers, got %d", len(product.Flags)) } }) t.Run("clear flags succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/products/%d", productID), map[string]any{ "flags": []string{}, }) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when clearing flags, got %d: %s", resp.StatusCode, string(body)) } product := fetchProduct(t, db, productID) if len(product.Flags) != 0 { t.Fatalf("expected all flags cleared, got %d", len(product.Flags)) } }) t.Run("delete product succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", productID), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when deleting product, got %d: %s", resp.StatusCode, string(body)) } var product entities.Product if err := db.First(&product, productID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { t.Fatalf("expected product deleted, got error %v", err) } var links int64 if err := db.Model(&entities.ProductSupplier{}).Where("product_id = ?", productID).Count(&links).Error; err != nil { t.Fatalf("failed counting product suppliers: %v", err) } if links != 0 { t.Fatalf("expected pivot cleared, found %d rows", links) } var flagCount int64 if err := db.Model(&entities.Flag{}). Where("flagable_id = ? AND flagable_type = ?", productID, entities.FlagableTypeProduct). Count(&flagCount).Error; err != nil { t.Fatalf("failed counting product flags: %v", err) } if flagCount != 0 { t.Fatalf("expected flags removed, found %d", flagCount) } }) t.Run("delete missing product returns 404", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/products/%d", productID), nil) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when deleting missing product, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("get deleted product returns 404", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/products/%d", productID), nil) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when fetching deleted product, got %d: %s", resp.StatusCode, string(body)) } }) }