mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
267 lines
9.8 KiB
Go
267 lines
9.8 KiB
Go
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)
|
|
productRepo := repository.NewProductRepository(db)
|
|
serviceRepo := repository.NewServiceRepository(db)
|
|
communityListingRepo := repository.NewCommunityListingRepository(db)
|
|
|
|
cacheService := service.NewMemoryCacheService()
|
|
matchingService = matching.NewService(matchRepo, negotiationRepo, resourceRepo, siteRepo, organizationRepo, productRepo, serviceRepo, communityListingRepo, 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))
|
|
})
|
|
})
|
|
})
|