package localdb import ( "path/filepath" "testing" "time" ) // seedUniversePricelist stores one active pricelist of the given source with the // supplied items and returns its local id. func seedUniversePricelist(t *testing.T, local *LocalDB, serverID uint, source, version string, created time.Time, items []LocalPricelistItem) uint { t.Helper() pl := &LocalPricelist{ ServerID: serverID, Source: source, Version: version, Name: version, CreatedAt: created, SyncedAt: created, IsActive: true, } if err := local.SaveLocalPricelist(pl); err != nil { t.Fatalf("save %s pricelist: %v", source, err) } stored, err := local.GetLocalPricelistByServerID(serverID) if err != nil { t.Fatalf("load %s pricelist: %v", source, err) } for i := range items { items[i].PricelistID = stored.ID } if err := local.SaveLocalPricelistItems(items); err != nil { t.Fatalf("save %s items: %v", source, err) } return stored.ID } func quality(v int) *int { return &v } func newUniverseDB(t *testing.T, name string) *LocalDB { t.Helper() local, err := New(filepath.Join(t.TempDir(), name)) if err != nil { t.Fatalf("open localdb: %v", err) } t.Cleanup(func() { _ = local.Close() }) return local } // The component universe is world ∪ estimate: a LOT the estimate pricelist does not // carry must still be selectable, otherwise BOM lot_mappings resolved from the // partnumber book get silently dropped on save. func TestComponentUniverseUnionsWorldAndEstimate(t *testing.T) { local := newUniverseDB(t, "universe_union.db") base := time.Now().Add(-time.Minute) seedUniversePricelist(t, local, 1, "estimate", "E-1", base, []LocalPricelistItem{ {LotName: "CPU_A", LotCategory: "CPU", LotDescription: "from estimate", Price: 100, PriceQuality: quality(8)}, {LotName: "SHARED_LOT", LotCategory: "MEM", LotDescription: "from estimate", Price: 50, PriceQuality: quality(9)}, }) seedUniversePricelist(t, local, 2, "world", "W-1", base, []LocalPricelistItem{ {LotName: "SHARED_LOT", LotCategory: "WRONG", LotDescription: "from world", Price: 55, PriceQuality: quality(1)}, {LotName: "NIC_WORLD_ONLY", LotCategory: "NIC", LotDescription: "from world", Price: 70, PriceQuality: quality(7)}, }) components, total, err := local.ListComponents(ComponentFilter{}, 0, 100) if err != nil { t.Fatalf("ListComponents: %v", err) } if total != 3 { t.Fatalf("expected 3 components (dedup on SHARED_LOT), got %d", total) } byLot := make(map[string]LocalComponent, len(components)) for _, c := range components { if _, dup := byLot[c.LotName]; dup { t.Fatalf("duplicate lot in universe: %s", c.LotName) } byLot[c.LotName] = c } worldOnly, ok := byLot["NIC_WORLD_ONLY"] if !ok { t.Fatal("world-only LOT missing from the component universe") } if worldOnly.PriceQuality == nil || *worldOnly.PriceQuality != 0 { t.Fatalf("world-only LOT must report price_quality 0, got %v", worldOnly.PriceQuality) } // The estimate row wins a collision, so its metadata stays authoritative. shared := byLot["SHARED_LOT"] if shared.Category != "MEM" || shared.LotDescription != "from estimate" { t.Fatalf("estimate row must win a collision, got category=%q description=%q", shared.Category, shared.LotDescription) } if shared.PriceQuality == nil || *shared.PriceQuality != 9 { t.Fatalf("estimate price_quality must survive a collision, got %v", shared.PriceQuality) } if got := local.CountComponents(); got != 3 { t.Fatalf("CountComponents: expected 3, got %d", got) } single, err := local.GetLocalComponent("nic_world_only") if err != nil { t.Fatalf("GetLocalComponent for a world-only LOT: %v", err) } if single.LotName != "NIC_WORLD_ONLY" { t.Fatalf("GetLocalComponent returned %q", single.LotName) } categories, err := local.GetLocalComponentCategories() if err != nil { t.Fatalf("GetLocalComponentCategories: %v", err) } if len(categories) != 3 { t.Fatalf("expected CPU/MEM/NIC, got %v", categories) } cats, err := local.GetLocalComponentCategoriesByLotNames([]string{"NIC_WORLD_ONLY", "SHARED_LOT"}) if err != nil { t.Fatalf("GetLocalComponentCategoriesByLotNames: %v", err) } if cats["NIC_WORLD_ONLY"] != "NIC" || cats["SHARED_LOT"] != "MEM" { t.Fatalf("unexpected categories: %v", cats) } found, err := local.SearchLocalComponents("world_only", 10) if err != nil || len(found) != 1 { t.Fatalf("SearchLocalComponents: got %d rows, err=%v", len(found), err) } inCategory, err := local.SearchLocalComponentsByCategory("NIC", "", 10) if err != nil || len(inCategory) != 1 { t.Fatalf("SearchLocalComponentsByCategory: got %d rows, err=%v", len(inCategory), err) } } // Only one of the two pricelists existing must not break the universe. func TestComponentUniverseWithSingleSource(t *testing.T) { t.Run("estimate only", func(t *testing.T) { local := newUniverseDB(t, "universe_estimate_only.db") seedUniversePricelist(t, local, 1, "estimate", "E-1", time.Now(), []LocalPricelistItem{ {LotName: "CPU_A", LotCategory: "CPU", Price: 100, PriceQuality: quality(8)}, }) if got := local.CountComponents(); got != 1 { t.Fatalf("expected 1 component, got %d", got) } }) t.Run("world only", func(t *testing.T) { local := newUniverseDB(t, "universe_world_only.db") seedUniversePricelist(t, local, 2, "world", "W-1", time.Now(), []LocalPricelistItem{ {LotName: "CPU_A", LotCategory: "CPU", Price: 100, PriceQuality: quality(8)}, }) components, _, err := local.ListComponents(ComponentFilter{}, 0, 10) if err != nil { t.Fatalf("ListComponents: %v", err) } if len(components) != 1 { t.Fatalf("expected 1 component, got %d", len(components)) } if components[0].PriceQuality == nil || *components[0].PriceQuality != 0 { t.Fatalf("world-only LOT must report price_quality 0, got %v", components[0].PriceQuality) } }) t.Run("neither", func(t *testing.T) { local := newUniverseDB(t, "universe_empty.db") if _, _, err := local.ListComponents(ComponentFilter{}, 0, 10); err == nil { t.Fatal("expected an error when no estimate and no world pricelist exists") } if got := local.CountComponents(); got != 0 { t.Fatalf("expected 0, got %d", got) } }) }