package handler_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "bugulma/backend/internal/matching/engine" "bugulma/backend/internal/testutils" "github.com/gin-gonic/gin" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "gorm.io/datatypes" "gorm.io/gorm" "bugulma/backend/internal/domain" "bugulma/backend/internal/handler" "bugulma/backend/internal/matching" "bugulma/backend/internal/repository" "bugulma/backend/internal/service" ) var _ = Describe("MatchingHandler", func() { var ( matchingHandler *handler.MatchingHandler matchingService *matching.Service matchRepo domain.MatchRepository resourceRepo domain.ResourceFlowRepository organizationRepo domain.OrganizationRepository siteRepo domain.SiteRepository router *gin.Engine db *gorm.DB ) BeforeEach(func() { gin.SetMode(gin.TestMode) // Setup PostgreSQL test database using pgtestdb // Each test gets an isolated database with migrations already applied db = testutils.SetupTestDBForGinkgo(GinkgoT()) matchRepo = repository.NewMatchRepository(db) resourceRepo = repository.NewResourceFlowRepository(db) organizationRepo = repository.NewOrganizationRepository(db) siteRepo = repository.NewSiteRepository(db) negotiationRepo := repository.NewNegotiationHistoryRepository(db) cacheService := service.NewMemoryCacheService() matchingService = matching.NewService(matchRepo, negotiationRepo, resourceRepo, siteRepo, organizationRepo, nil, nil, nil, nil) matchingHandler = handler.NewMatchingHandler(matchingService, cacheService) router = gin.New() router.POST("/matches/find", matchingHandler.FindMatches) router.POST("/matches", matchingHandler.CreateMatchFromQuery) router.PUT("/matches/:matchId/status", matchingHandler.UpdateMatchStatus) }) AfterEach(func() { // pgtestdb automatically cleans up the database after each test // No manual cleanup needed }) Describe("FindMatches", func() { It("should find matches for a resource type", func() { // Create organizations first (required for foreign keys) org1 := &domain.Organization{ID: "org-1", Name: "Org 1"} org2 := &domain.Organization{ID: "org-2", Name: "Org 2"} Expect(organizationRepo.Create(context.TODO(), org1)).To(Succeed()) Expect(organizationRepo.Create(context.TODO(), org2)).To(Succeed()) // Create sites with OwnerOrganizationID site1 := &domain.Site{ID: "site-1", Latitude: 52.5200, Longitude: 13.4050, OwnerOrganizationID: "org-1"} site2 := &domain.Site{ID: "site-2", Latitude: 52.5210, Longitude: 13.4060, OwnerOrganizationID: "org-2"} Expect(siteRepo.Create(context.TODO(), site1)).To(Succeed()) Expect(siteRepo.Create(context.TODO(), site2)).To(Succeed()) // Create output resource output := &domain.ResourceFlow{ ID: "res-1", OrganizationID: "org-1", SiteID: "site-1", Direction: domain.DirectionOutput, Type: domain.TypeHeat, Quantity: datatypes.JSON(`{"amount": 100, "unit": "kWh"}`), } Expect(resourceRepo.Create(context.TODO(), output)).To(Succeed()) // Create input resource input := &domain.ResourceFlow{ ID: "res-2", OrganizationID: "org-2", SiteID: "site-2", Direction: domain.DirectionInput, Type: domain.TypeHeat, Quantity: datatypes.JSON(`{"amount": 50, "unit": "kWh"}`), } Expect(resourceRepo.Create(context.TODO(), input)).To(Succeed()) reqBody := handler.MatchQueryRequest{ Resource: struct { Type string `json:"type" binding:"required"` Direction string `json:"direction" binding:"required"` SiteID string `json:"site_id,omitempty"` TemperatureRange *struct { MinCelsius float64 `json:"min_celsius"` MaxCelsius float64 `json:"max_celsius"` } `json:"temperature_range,omitempty"` QuantityRange *struct { MinAmount float64 `json:"min_amount"` MaxAmount float64 `json:"max_amount"` Unit string `json:"unit"` } `json:"quantity_range,omitempty"` }{ Type: "heat", Direction: "input", SiteID: "site-1", // Specify site to match against }, Constraints: struct { MaxDistanceKm float64 `json:"max_distance_km"` MinEconomicValue float64 `json:"min_economic_value"` PrecisionPreference []string `json:"precision_preference"` IncludeServices bool `json:"include_services"` }{ MaxDistanceKm: 10.0, MinEconomicValue: 0.1, }, Pagination: struct { Limit int `json:"limit"` Offset int `json:"offset"` }{ Limit: 10, }, } body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/matches/find", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) Expect(w.Code).To(Equal(http.StatusOK)) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) Expect(err).NotTo(HaveOccurred()) Expect(resp).To(HaveKey("matches")) Expect(resp).To(HaveKey("metadata")) }) }) Describe("CreateMatch", func() { It("should create a match", func() { // Create organizations first (required for foreign keys) org1 := &domain.Organization{ID: "org-1", Name: "Org 1"} org2 := &domain.Organization{ID: "org-2", Name: "Org 2"} Expect(organizationRepo.Create(context.TODO(), org1)).To(Succeed()) Expect(organizationRepo.Create(context.TODO(), org2)).To(Succeed()) // Create sites with OwnerOrganizationID site1 := &domain.Site{ID: "site-1", Latitude: 52.5200, Longitude: 13.4050, OwnerOrganizationID: "org-1"} site2 := &domain.Site{ID: "site-2", Latitude: 52.5210, Longitude: 13.4060, OwnerOrganizationID: "org-2"} Expect(siteRepo.Create(context.TODO(), site1)).To(Succeed()) Expect(siteRepo.Create(context.TODO(), site2)).To(Succeed()) // Create output resource output := &domain.ResourceFlow{ ID: "res-1", OrganizationID: "org-1", SiteID: "site-1", Direction: domain.DirectionOutput, Type: domain.TypeHeat, Quantity: datatypes.JSON(`{"amount": 100, "unit": "kWh"}`), } Expect(resourceRepo.Create(context.TODO(), output)).To(Succeed()) // Create input resource input := &domain.ResourceFlow{ ID: "res-2", OrganizationID: "org-2", SiteID: "site-2", Direction: domain.DirectionInput, Type: domain.TypeHeat, Quantity: datatypes.JSON(`{"amount": 50, "unit": "kWh"}`), } Expect(resourceRepo.Create(context.TODO(), input)).To(Succeed()) reqBody := struct { SourceFlowID string `json:"source_flow_id"` TargetFlowID string `json:"target_flow_id"` }{ SourceFlowID: "res-1", TargetFlowID: "res-2", } body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/matches", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) Expect(w.Code).To(Equal(http.StatusCreated)) var resp domain.Match err := json.Unmarshal(w.Body.Bytes(), &resp) Expect(err).NotTo(HaveOccurred()) Expect(resp.ID).NotTo(BeEmpty()) Expect(resp.SourceResourceID).To(Equal("res-1")) Expect(resp.TargetResourceID).To(Equal("res-2")) }) }) Describe("UpdateStatus", func() { It("should update match status", func() { // Create organizations, sites, and resource flows first (required for foreign keys) org1 := &domain.Organization{ID: "org-1", Name: "Org 1"} org2 := &domain.Organization{ID: "org-2", Name: "Org 2"} site1 := &domain.Site{ID: "site-1", Name: "Site 1", Latitude: 52.5200, Longitude: 13.4050, OwnerOrganizationID: "org-1"} site2 := &domain.Site{ID: "site-2", Name: "Site 2", Latitude: 52.5210, Longitude: 13.4060, OwnerOrganizationID: "org-2"} Expect(organizationRepo.Create(context.TODO(), org1)).To(Succeed()) Expect(organizationRepo.Create(context.TODO(), org2)).To(Succeed()) Expect(siteRepo.Create(context.TODO(), site1)).To(Succeed()) Expect(siteRepo.Create(context.TODO(), site2)).To(Succeed()) // Create resource flows res1 := &domain.ResourceFlow{ID: "res-1", OrganizationID: "org-1", SiteID: "site-1", Direction: domain.DirectionOutput, Type: domain.TypeHeat, Quantity: datatypes.JSON(`{"amount": 100, "unit": "kWh"}`)} res2 := &domain.ResourceFlow{ID: "res-2", OrganizationID: "org-2", SiteID: "site-2", Direction: domain.DirectionInput, Type: domain.TypeHeat, Quantity: datatypes.JSON(`{"amount": 50, "unit": "kWh"}`)} Expect(resourceRepo.Create(context.TODO(), res1)).To(Succeed()) Expect(resourceRepo.Create(context.TODO(), res2)).To(Succeed()) // Create a match using the matching service (to properly initialize it) candidate := &engine.Candidate{ SourceFlow: res1, TargetFlow: res2, DistanceKm: 0.1, CompatibilityScore: 0.8, EconomicScore: 0.7, TemporalScore: 0.9, QualityScore: 0.85, OverallScore: 0.8, } match, err := matchingService.CreateMatch(context.TODO(), candidate, "test-user") Expect(err).NotTo(HaveOccurred()) Expect(match).NotTo(BeNil()) reqBody := map[string]interface{}{ "status": "negotiating", "actor": "test-user", "notes": "Test negotiation start", } body, _ := json.Marshal(reqBody) req, _ := http.NewRequest("PUT", "/matches/"+match.ID+"/status", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) Expect(w.Code).To(Equal(http.StatusOK)) }) }) })