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 TestNonstockIntegration(t *testing.T) { app, db := setupIntegrationApp(t) uomID := createUom(t, app, "Unit Piece") altUomID := createUom(t, app, "Unit Box") supplierID1 := createSupplier(t, app, "Nonstock Supplier One", "ns1", string(utils.SupplierCategoryBOP)) supplierID2 := createSupplier(t, app, "Nonstock Supplier Two", "ns2", string(utils.SupplierCategoryBOP)) supplierID3 := createSupplier(t, app, "Nonstock Supplier Three", "ns3", string(utils.SupplierCategoryBOP)) sapronakSupplierID := createSupplier(t, app, "SAPRONAK Supplier", "sap1", string(utils.SupplierCategorySapronak)) nonstockFlags := []string{string(utils.FlagEkspedisi)} t.Run("create nonstock without suppliers succeeds with empty relations", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ "name": "Supplierless Nonstock", "uom_id": uomID, }) if resp.StatusCode != fiber.StatusCreated { t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body)) } id := parseID(t, body) ns := fetchNonstock(t, db, id) if len(ns.Suppliers) != 0 { t.Fatalf("expected no suppliers persisted, found %d", len(ns.Suppliers)) } if len(ns.Flags) != 0 { t.Fatalf("expected no flags persisted, found %d", len(ns.Flags)) } resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", id), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode) } }) t.Run("create nonstock with unknown supplier fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ "name": "Unknown Supplier Nonstock", "uom_id": uomID, "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 nonstock with sapronak supplier fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ "name": "Invalid Category Nonstock", "uom_id": uomID, "supplier_ids": []uint{sapronakSupplierID}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when supplier category is not BOP, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("create nonstock with invalid flags fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{ "name": "Invalid Flag Nonstock", "uom_id": uomID, "supplier_ids": []uint{supplierID1}, "flags": []string{"UNKNOWN"}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body)) } }) var nonstockID uint t.Run("create nonstock succeeds", func(t *testing.T) { nonstockID = createNonstock(t, app, "Layer Feed", uomID, []uint{supplierID1, supplierID2, supplierID1}, nonstockFlags) if nonstockID == 0 { t.Fatal("expected nonstock id to be non zero") } ns := fetchNonstock(t, db, nonstockID) if ns.Name != "Layer Feed" { t.Fatalf("expected name Layer Feed, got %q", ns.Name) } if ns.UomId != uomID { t.Fatalf("expected uom_id %d, got %d", uomID, ns.UomId) } if ns.CreatedBy != 1 { t.Fatalf("expected created_by 1, got %d", ns.CreatedBy) } if len(ns.Suppliers) != 2 { t.Fatalf("expected 2 unique suppliers, got %d", len(ns.Suppliers)) } if len(ns.Flags) != len(nonstockFlags) { t.Fatalf("expected %d flags, got %d", len(nonstockFlags), len(ns.Flags)) } expectedFlags := make(map[string]struct{}, len(nonstockFlags)) for _, flag := range nonstockFlags { expectedFlags[strings.ToUpper(flag)] = struct{}{} } for _, flag := range ns.Flags { upper := strings.ToUpper(flag.Name) if _, ok := expectedFlags[upper]; !ok { t.Fatalf("unexpected flag stored: %s", upper) } delete(expectedFlags, upper) } if len(expectedFlags) != 0 { t.Fatalf("missing flags after create: %v", expectedFlags) } }) t.Run("get nonstock detail includes suppliers", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when fetching nonstock, got %d: %s", resp.StatusCode, string(body)) } var payload struct { Data struct { Id uint `json:"id"` Name string `json:"name"` UomID uint `json:"uom_id"` 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 nonstock detail: %v", err) } if payload.Data.Id != nonstockID { t.Fatalf("expected id %d, got %d", nonstockID, payload.Data.Id) } if payload.Data.UomID != uomID { t.Fatalf("expected response uom_id %d, got %d", uomID, payload.Data.UomID) } 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(nonstockFlags) { t.Fatalf("expected %d flags in response, got %d", len(nonstockFlags), len(payload.Data.Flags)) } expected := make(map[string]struct{}, len(nonstockFlags)) for _, flag := range nonstockFlags { 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 flags in response: %v", expected) } }) t.Run("update nonstock with invalid uom fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ "uom_id": 99999, }) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when updating with invalid uom, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("update nonstock with invalid supplier fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ "supplier_ids": []uint{supplierID1, 99999}, }) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when updating with invalid supplier, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("update nonstock with sapronak supplier fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ "supplier_ids": []uint{sapronakSupplierID}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when updating with non-BOP supplier, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("update nonstock with invalid flags fails", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ "flags": []string{"BAD"}, }) if resp.StatusCode != fiber.StatusBadRequest { t.Fatalf("expected 400 when updating flags invalid, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("update nonstock name uom and suppliers succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ "name": "Layer Feed Premium", "uom_id": altUomID, "supplier_ids": []uint{supplierID3}, "flags": nonstockFlags, }) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when updating nonstock, got %d: %s", resp.StatusCode, string(body)) } ns := fetchNonstock(t, db, nonstockID) if ns.Name != "Layer Feed Premium" { t.Fatalf("expected name Layer Feed Premium, got %q", ns.Name) } if ns.UomId != altUomID { t.Fatalf("expected uom_id %d, got %d", altUomID, ns.UomId) } if len(ns.Suppliers) != 1 || ns.Suppliers[0].Id != supplierID3 { t.Fatalf("expected suppliers to contain only %d", supplierID3) } if len(ns.Flags) != len(nonstockFlags) { t.Fatalf("expected flags retained, got %d", len(ns.Flags)) } }) t.Run("clear suppliers succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), 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)) } ns := fetchNonstock(t, db, nonstockID) if len(ns.Suppliers) != 0 { t.Fatalf("expected suppliers to be cleared, got %d entries", len(ns.Suppliers)) } if len(ns.Flags) != len(nonstockFlags) { t.Fatalf("expected flags unaffected, got %d", len(ns.Flags)) } }) t.Run("clear nonstock flags succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{ "flags": []string{}, }) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when clearing nonstock flags, got %d: %s", resp.StatusCode, string(body)) } ns := fetchNonstock(t, db, nonstockID) if len(ns.Flags) != 0 { t.Fatalf("expected flags cleared, got %d", len(ns.Flags)) } }) t.Run("delete nonstock succeeds", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when deleting nonstock, got %d: %s", resp.StatusCode, string(body)) } var ns entities.Nonstock if err := db.First(&ns, nonstockID).Error; !errors.Is(err, gorm.ErrRecordNotFound) { t.Fatalf("expected nonstock to be deleted, got error %v", err) } var links int64 if err := db.Model(&entities.NonstockSupplier{}).Where("nonstock_id = ?", nonstockID).Count(&links).Error; err != nil { t.Fatalf("failed counting nonstock suppliers: %v", err) } if links != 0 { t.Fatalf("expected link table cleared, found %d rows", links) } var flagCount int64 if err := db.Model(&entities.Flag{}). Where("flagable_id = ? AND flagable_type = ?", nonstockID, entities.FlagableTypeNonstock). Count(&flagCount).Error; err != nil { t.Fatalf("failed counting nonstock flags: %v", err) } if flagCount != 0 { t.Fatalf("expected flags removed, found %d", flagCount) } }) t.Run("deleting nonstock twice returns 404", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when deleting missing nonstock, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("fetching deleted nonstock returns 404", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil) if resp.StatusCode != fiber.StatusNotFound { t.Fatalf("expected 404 when fetching deleted nonstock, got %d: %s", resp.StatusCode, string(body)) } }) t.Run("cleanup additional supplier references", func(t *testing.T) { resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID3), nil) if resp.StatusCode != fiber.StatusOK { t.Fatalf("expected 200 when deleting supplier, got %d: %s", resp.StatusCode, string(body)) } doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", sapronakSupplierID), nil) }) }