Files
lti-api/test/integration/master_data/product_test.go
T

411 lines
16 KiB
Go

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))
}
})
}