diff --git a/models/booking/booking_mongo_accessor.go b/models/booking/booking_mongo_accessor.go index 1713264..8bcc24b 100644 --- a/models/booking/booking_mongo_accessor.go +++ b/models/booking/booking_mongo_accessor.go @@ -11,13 +11,13 @@ import ( "cloud.o-forge.io/core/oc-lib/tools" ) -type bookingMongoAccessor struct { +type BookingMongoAccessor struct { utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) } -// New creates a new instance of the bookingMongoAccessor -func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor { - return &bookingMongoAccessor{ +// New creates a new instance of the BookingMongoAccessor +func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor { + return &BookingMongoAccessor{ AbstractAccessor: utils.AbstractAccessor{ Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type Request: request, @@ -29,11 +29,11 @@ func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor { /* * Nothing special here, just the basic CRUD operations */ -func (a *bookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { +func (a *BookingMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { return utils.GenericDeleteOne(id, a) } -func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { +func (a *BookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { if set.(*Booking).State == 0 { return nil, 400, errors.New("state is required") } @@ -41,15 +41,15 @@ func (a *bookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.D return utils.GenericUpdateOne(realSet, id, a, &Booking{}) } -func (a *bookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { +func (a *BookingMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { return utils.GenericStoreOne(data, a) } -func (a *bookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { +func (a *BookingMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { return utils.GenericStoreOne(data, a) } -func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { +func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) { now := time.Now() now = now.Add(time.Second * -60) @@ -67,15 +67,15 @@ func (a *bookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { }, a) } -func (a *bookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { +func (a *BookingMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { return utils.GenericLoadAll[*Booking](a.getExec(), isDraft, a) } -func (a *bookingMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { +func (a *BookingMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { return utils.GenericSearch[*Booking](filters, search, (&Booking{}).GetObjectFilters(search), a.getExec(), isDraft, a) } -func (a *bookingMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject { +func (a *BookingMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject { now := time.Now() now = now.Add(time.Second * -60) diff --git a/models/booking/tests/booking_test.go b/models/booking/tests/booking_test.go new file mode 100644 index 0000000..f8ead5f --- /dev/null +++ b/models/booking/tests/booking_test.go @@ -0,0 +1,87 @@ +package booking_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "cloud.o-forge.io/core/oc-lib/models/booking" + "cloud.o-forge.io/core/oc-lib/models/common/enum" + "cloud.o-forge.io/core/oc-lib/models/utils" + "cloud.o-forge.io/core/oc-lib/tools" +) + +func TestBooking_GetDurations(t *testing.T) { + start := time.Now().Add(-2 * time.Hour) + end := start.Add(1 * time.Hour) + realStart := start.Add(30 * time.Minute) + realEnd := realStart.Add(90 * time.Minute) + + b := &booking.Booking{ + ExpectedStartDate: start, + ExpectedEndDate: &end, + RealStartDate: &realStart, + RealEndDate: &realEnd, + } + + assert.Equal(t, 30*time.Minute, b.GetDelayForLaunch()) + assert.Equal(t, 90*time.Minute, b.GetRealDuration()) + assert.Equal(t, end.Sub(start), b.GetUsualDuration()) + assert.Equal(t, b.GetRealDuration()-b.GetUsualDuration(), b.GetDelayOnDuration()) + assert.Equal(t, realEnd.Sub(start), b.GetDelayForFinishing()) +} + +func TestBooking_GetAccessor(t *testing.T) { + req := &tools.APIRequest{} + b := &booking.Booking{} + accessor := b.GetAccessor(req) + + assert.NotNil(t, accessor) + assert.Equal(t, tools.BOOKING, accessor.(*booking.BookingMongoAccessor).Type) +} + +func TestBooking_VerifyAuth(t *testing.T) { + assert.True(t, (&booking.Booking{}).VerifyAuth(nil)) +} + +func TestBooking_StoreDraftDefault(t *testing.T) { + b := &booking.Booking{} + b.StoreDraftDefault() + assert.False(t, b.IsDraft) +} + +func TestBooking_CanUpdate(t *testing.T) { + now := time.Now() + b := &booking.Booking{ + State: enum.SCHEDULED, + AbstractObject: utils.AbstractObject{IsDraft: false}, + RealStartDate: &now, + } + + set := &booking.Booking{ + State: enum.DELAYED, + RealStartDate: &now, + } + + ok, result := b.CanUpdate(set) + assert.True(t, ok) + assert.Equal(t, enum.DELAYED, result.(*booking.Booking).State) +} + +func TestBooking_CanDelete(t *testing.T) { + b := &booking.Booking{AbstractObject: utils.AbstractObject{IsDraft: true}} + assert.True(t, b.CanDelete()) + + b.IsDraft = false + assert.False(t, b.CanDelete()) +} + +func TestNewAccessor(t *testing.T) { + req := &tools.APIRequest{} + accessor := booking.NewAccessor(req) + + assert.NotNil(t, accessor) + assert.Equal(t, tools.BOOKING, accessor.Type) + assert.Equal(t, req, accessor.Request) +} diff --git a/models/common/pricing/interfaces.go b/models/common/pricing/interfaces.go old mode 100644 new mode 100755 diff --git a/models/common/pricing/pricing_profile.go b/models/common/pricing/pricing_profile.go old mode 100644 new mode 100755 diff --git a/models/common/pricing/pricing_strategy.go b/models/common/pricing/pricing_strategy.go old mode 100644 new mode 100755 diff --git a/models/common/pricing/tests/pricing_test.go b/models/common/pricing/tests/pricing_test.go new file mode 100644 index 0000000..105b5d1 --- /dev/null +++ b/models/common/pricing/tests/pricing_test.go @@ -0,0 +1,129 @@ +package pricing_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "cloud.o-forge.io/core/oc-lib/models/common/pricing" +) + +type DummyStrategy int + +func (d DummyStrategy) GetStrategy() string { return "DUMMY" } +func (d DummyStrategy) GetStrategyValue() int { return int(d) } + +func TestBuyingStrategy_String(t *testing.T) { + assert.Equal(t, "UNLIMITED", pricing.UNLIMITED.String()) + assert.Equal(t, "SUBSCRIPTION", pricing.SUBSCRIPTION.String()) + assert.Equal(t, "PAY PER USE", pricing.PAY_PER_USE.String()) +} + +func TestBuyingStrategyList(t *testing.T) { + list := pricing.BuyingStrategyList() + assert.Equal(t, 3, len(list)) + assert.Contains(t, list, pricing.SUBSCRIPTION) +} + +func TestTimePricingStrategy_String(t *testing.T) { + assert.Equal(t, "ONCE", pricing.ONCE.String()) + assert.Equal(t, "PER SECOND", pricing.PER_SECOND.String()) + assert.Equal(t, "PER MONTH", pricing.PER_MONTH.String()) +} + +func TestTimePricingStrategyList(t *testing.T) { + list := pricing.TimePricingStrategyList() + assert.Equal(t, 7, len(list)) + assert.Contains(t, list, pricing.PER_DAY) +} + +func TestTimePricingStrategy_Methods(t *testing.T) { + ts := pricing.PER_MINUTE + assert.Equal(t, "PER_MINUTE", ts.GetStrategy()) + assert.Equal(t, 2, ts.GetStrategyValue()) +} + +func Test_getAverageTimeInSecond_WithEnd(t *testing.T) { + start := time.Now() + end := start.Add(30 * time.Minute) + + _, err := pricing.BookingEstimation(pricing.PER_MINUTE, 2.0, 1200, start, &end) + assert.NoError(t, err) +} + +func Test_getAverageTimeInSecond_WithoutEnd(t *testing.T) { + start := time.Now() + + // getAverageTimeInSecond is tested via BookingEstimation + price, err := pricing.BookingEstimation(pricing.PER_HOUR, 10.0, 100, start, nil) + assert.NoError(t, err) + assert.True(t, price > 0) +} + +func TestBookingEstimation(t *testing.T) { + start := time.Now() + end := start.Add(2 * time.Hour) + strategies := map[pricing.TimePricingStrategy]float64{ + pricing.ONCE: 50, + pricing.PER_HOUR: 10, + pricing.PER_MINUTE: 1, + pricing.PER_SECOND: 0.1, + pricing.PER_DAY: 100, + pricing.PER_WEEK: 500, + pricing.PER_MONTH: 2000, + } + + for strategy, price := range strategies { + t.Run(strategy.String(), func(t *testing.T) { + cost, err := pricing.BookingEstimation(strategy, price, 3600, start, &end) + assert.NoError(t, err) + assert.True(t, cost >= 0) + }) + } + + _, err := pricing.BookingEstimation(999, 10, 3600, start, &end) + assert.Error(t, err) +} + +func TestPricingStrategy_Getters(t *testing.T) { + ps := pricing.PricingStrategy[DummyStrategy]{ + Price: 20, + Currency: "USD", + BuyingStrategy: pricing.SUBSCRIPTION, + TimePricingStrategy: pricing.PER_MINUTE, + OverrideStrategy: DummyStrategy(1), + } + + assert.Equal(t, pricing.SUBSCRIPTION, ps.GetBuyingStrategy()) + assert.Equal(t, pricing.PER_MINUTE, ps.GetTimePricingStrategy()) + assert.Equal(t, DummyStrategy(1), ps.GetOverrideStrategy()) +} + +func TestPricingStrategy_GetPrice(t *testing.T) { + start := time.Now() + end := start.Add(1 * time.Hour) + + // SUBSCRIPTION case + ps := pricing.PricingStrategy[DummyStrategy]{ + Price: 5, + BuyingStrategy: pricing.SUBSCRIPTION, + TimePricingStrategy: pricing.PER_HOUR, + } + + p, err := ps.GetPrice(2, 3600, start, &end) + assert.NoError(t, err) + assert.True(t, p > 0) + + // UNLIMITED case + ps.BuyingStrategy = pricing.UNLIMITED + p, err = ps.GetPrice(10, 0, start, &end) + assert.NoError(t, err) + assert.Equal(t, 5.0, p) + + // PAY_PER_USE case + ps.BuyingStrategy = pricing.PAY_PER_USE + p, err = ps.GetPrice(3, 0, start, &end) + assert.NoError(t, err) + assert.Equal(t, 15.0, p) +} diff --git a/models/resources/compute.go b/models/resources/compute.go index 22e521e..c1f4c8e 100755 --- a/models/resources/compute.go +++ b/models/resources/compute.go @@ -151,9 +151,9 @@ func (r *PricedComputeResource) GetPrice() (float64, error) { if len(r.PricingProfiles) == 0 { return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID) } - r.SelectedPricing = &r.PricingProfiles[0] + r.SelectedPricing = r.PricingProfiles[0] } - pricing := *r.SelectedPricing + pricing := r.SelectedPricing price := float64(0) for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} { for model, amountOfData := range l { diff --git a/models/resources/data.go b/models/resources/data.go index 8734e59..130b244 100755 --- a/models/resources/data.go +++ b/models/resources/data.go @@ -164,9 +164,9 @@ func (r *PricedDataResource) GetPrice() (float64, error) { if len(r.PricingProfiles) == 0 { return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID) } - r.SelectedPricing = &r.PricingProfiles[0] + r.SelectedPricing = r.PricingProfiles[0] } - pricing := *r.SelectedPricing + pricing := r.SelectedPricing var err error amountOfData := float64(1) if pricing.GetOverrideStrategyValue() >= 0 { diff --git a/models/resources/priced_resource.go b/models/resources/priced_resource.go index ab0bfe6..5265cce 100755 --- a/models/resources/priced_resource.go +++ b/models/resources/priced_resource.go @@ -14,7 +14,7 @@ type PricedResource struct { Logo string `json:"logo,omitempty" bson:"logo,omitempty"` InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"` PricingProfiles []pricing.PricingProfileITF `json:"pricing_profiles,omitempty" bson:"pricing_profiles,omitempty"` - SelectedPricing *pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"` + SelectedPricing pricing.PricingProfileITF `json:"selected_pricing,omitempty" bson:"selected_pricing,omitempty"` ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"` UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"` UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"` @@ -39,7 +39,7 @@ func (abs *PricedResource) IsPurchased() bool { if abs.SelectedPricing == nil { return false } - return (*abs.SelectedPricing).IsPurchased() + return (abs.SelectedPricing).IsPurchased() } func (abs *PricedResource) GetLocationEnd() *time.Time { @@ -86,8 +86,8 @@ func (r *PricedResource) GetPrice() (float64, error) { if len(r.PricingProfiles) == 0 { return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID) } - r.SelectedPricing = &r.PricingProfiles[0] + r.SelectedPricing = r.PricingProfiles[0] } - pricing := *r.SelectedPricing + pricing := r.SelectedPricing return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd) } diff --git a/models/resources/purchase_resource/purchase_resource_accessor.go b/models/resources/purchase_resource/purchase_resource_accessor.go index 8e87a93..3f3a625 100644 --- a/models/resources/purchase_resource/purchase_resource_accessor.go +++ b/models/resources/purchase_resource/purchase_resource_accessor.go @@ -9,13 +9,13 @@ import ( "cloud.o-forge.io/core/oc-lib/tools" ) -type purchaseResourceMongoAccessor struct { +type PurchaseResourceMongoAccessor struct { utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) } // New creates a new instance of the bookingMongoAccessor -func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor { - return &purchaseResourceMongoAccessor{ +func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor { + return &PurchaseResourceMongoAccessor{ AbstractAccessor: utils.AbstractAccessor{ Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type Request: request, @@ -27,23 +27,23 @@ func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor { /* * Nothing special here, just the basic CRUD operations */ -func (a *purchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { return utils.GenericDeleteOne(id, a) } -func (a *purchaseResourceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { return utils.GenericUpdateOne(set, id, a, &PurchaseResource{}) } -func (a *purchaseResourceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { return utils.GenericStoreOne(data, a) } -func (a *purchaseResourceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { return utils.GenericStoreOne(data, a) } -func (a *purchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { return utils.GenericLoadOne[*PurchaseResource](id, func(d utils.DBObject) (utils.DBObject, int, error) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { utils.GenericDeleteOne(id, a) @@ -53,15 +53,15 @@ func (a *purchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int, }, a) } -func (a *purchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { return utils.GenericLoadAll[*PurchaseResource](a.getExec(), isDraft, a) } -func (a *purchaseResourceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { +func (a *PurchaseResourceMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { return utils.GenericSearch[*PurchaseResource](filters, search, (&PurchaseResource{}).GetObjectFilters(search), a.getExec(), isDraft, a) } -func (a *purchaseResourceMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject { +func (a *PurchaseResourceMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { utils.GenericDeleteOne(d.GetID(), a) diff --git a/models/resources/purchase_resource/tests/purchase_resource_test.go b/models/resources/purchase_resource/tests/purchase_resource_test.go new file mode 100644 index 0000000..ec6abae --- /dev/null +++ b/models/resources/purchase_resource/tests/purchase_resource_test.go @@ -0,0 +1,56 @@ +package purchase_resource_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "cloud.o-forge.io/core/oc-lib/models/purchase_resource" + "cloud.o-forge.io/core/oc-lib/models/utils" + "cloud.o-forge.io/core/oc-lib/tools" +) + +func TestGetAccessor(t *testing.T) { + req := &tools.APIRequest{} + res := &purchase_resource.PurchaseResource{} + accessor := res.GetAccessor(req) + + assert.NotNil(t, accessor) + assert.Equal(t, tools.PURCHASE_RESOURCE, accessor.(*purchase_resource.PurchaseResourceMongoAccessor).Type) +} + +func TestCanUpdate(t *testing.T) { + set := &purchase_resource.PurchaseResource{ResourceID: "id"} + r := &purchase_resource.PurchaseResource{ + AbstractObject: utils.AbstractObject{IsDraft: true}, + } + can, updated := r.CanUpdate(set) + assert.True(t, can) + assert.Equal(t, set, updated) + + r.IsDraft = false + can, _ = r.CanUpdate(set) + assert.False(t, can) +} + +func TestCanDelete(t *testing.T) { + now := time.Now().UTC() + past := now.Add(-1 * time.Hour) + future := now.Add(1 * time.Hour) + + t.Run("nil EndDate", func(t *testing.T) { + r := &purchase_resource.PurchaseResource{} + assert.False(t, r.CanDelete()) + }) + + t.Run("EndDate in past", func(t *testing.T) { + r := &purchase_resource.PurchaseResource{EndDate: &past} + assert.True(t, r.CanDelete()) + }) + + t.Run("EndDate in future", func(t *testing.T) { + r := &purchase_resource.PurchaseResource{EndDate: &future} + assert.False(t, r.CanDelete()) + }) +} diff --git a/models/resources/resource.go b/models/resources/resource.go index 29386d6..f0bce28 100755 --- a/models/resources/resource.go +++ b/models/resources/resource.go @@ -92,7 +92,7 @@ func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.A if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" { return } - abs.Instances = verifyAuthAction[T](abs.Instances, request) + abs.Instances = VerifyAuthAction[T](abs.Instances, request) } func (d *AbstractInstanciatedResource[T]) Trim() { @@ -105,10 +105,10 @@ func (d *AbstractInstanciatedResource[T]) Trim() { } func (abs *AbstractInstanciatedResource[T]) VerifyAuth(request *tools.APIRequest) bool { - return len(verifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request) + return len(VerifyAuthAction[T](abs.Instances, request)) > 0 || abs.AbstractObject.VerifyAuth(request) } -func verifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T { +func VerifyAuthAction[T ResourceInstanceITF](baseInstance []T, request *tools.APIRequest) []T { instances := []T{} for _, instance := range baseInstance { _, peerGroups := instance.GetPeerGroups() diff --git a/models/resources/resource_accessor.go b/models/resources/resource_accessor.go index 645f6c5..338c0b1 100755 --- a/models/resources/resource_accessor.go +++ b/models/resources/resource_accessor.go @@ -9,17 +9,17 @@ import ( "cloud.o-forge.io/core/oc-lib/tools" ) -type resourceMongoAccessor[T ResourceInterface] struct { +type ResourceMongoAccessor[T ResourceInterface] struct { utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) generateData func() utils.DBObject } // New creates a new instance of the computeMongoAccessor -func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *resourceMongoAccessor[T] { +func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIRequest, g func() utils.DBObject) *ResourceMongoAccessor[T] { if !slices.Contains([]tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE, tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE}, t) { return nil } - return &resourceMongoAccessor[T]{ + return &ResourceMongoAccessor[T]{ AbstractAccessor: utils.AbstractAccessor{ Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Request: request, @@ -32,39 +32,39 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques /* * Nothing special here, just the basic CRUD operations */ -func (dca *resourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) { +func (dca *ResourceMongoAccessor[T]) DeleteOne(id string) (utils.DBObject, int, error) { return utils.GenericDeleteOne(id, dca) } -func (dca *resourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { +func (dca *ResourceMongoAccessor[T]) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { set.(T).Trim() return utils.GenericUpdateOne(set, id, dca, dca.generateData()) } -func (dca *resourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { +func (dca *ResourceMongoAccessor[T]) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { data.(T).Trim() return utils.GenericStoreOne(data, dca) } -func (dca *resourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { +func (dca *ResourceMongoAccessor[T]) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { return dca.StoreOne(data) } -func (dca *resourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) { +func (dca *ResourceMongoAccessor[T]) LoadOne(id string) (utils.DBObject, int, error) { return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) { d.(T).SetAllowedInstances(dca.Request) return d, 200, nil }, dca) } -func (wfa *resourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { +func (wfa *ResourceMongoAccessor[T]) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject { d.(T).SetAllowedInstances(wfa.Request) return d }, isDraft, wfa) } -func (wfa *resourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { +func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { if filters == nil && search == "*" { return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject { d.(T).SetAllowedInstances(wfa.Request) @@ -78,7 +78,7 @@ func (wfa *resourceMongoAccessor[T]) Search(filters *dbs.Filters, search string, }, isDraft, wfa) } -func (abs *resourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters { +func (abs *ResourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters { return &dbs.Filters{ Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided "abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}}, diff --git a/models/resources/storage.go b/models/resources/storage.go index 6d7c8ce..5e1476b 100755 --- a/models/resources/storage.go +++ b/models/resources/storage.go @@ -183,9 +183,9 @@ func (r *PricedStorageResource) GetPrice() (float64, error) { if len(r.PricingProfiles) == 0 { return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID) } - r.SelectedPricing = &r.PricingProfiles[0] + r.SelectedPricing = r.PricingProfiles[0] } - pricing := *r.SelectedPricing + pricing := r.SelectedPricing var err error amountOfData := float64(1) if pricing.GetOverrideStrategyValue() >= 0 { diff --git a/models/resources/tests/compute_test.go b/models/resources/tests/compute_test.go new file mode 100644 index 0000000..ff33362 --- /dev/null +++ b/models/resources/tests/compute_test.go @@ -0,0 +1,119 @@ +package resources_test + +import ( + "testing" + "time" + + "cloud.o-forge.io/core/oc-lib/models/common/models" + "cloud.o-forge.io/core/oc-lib/models/common/pricing" + "cloud.o-forge.io/core/oc-lib/models/resources" + "cloud.o-forge.io/core/oc-lib/tools" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestComputeResource_GetType(t *testing.T) { + r := &resources.ComputeResource{} + assert.Equal(t, tools.COMPUTE_RESOURCE.String(), r.GetType()) +} + +func TestComputeResource_GetAccessor(t *testing.T) { + req := &tools.APIRequest{} + cr := &resources.ComputeResource{} + accessor := cr.GetAccessor(req) + assert.NotNil(t, accessor) +} + +func TestComputeResource_ConvertToPricedResource(t *testing.T) { + req := &tools.APIRequest{} + cr := &resources.ComputeResource{} + cr.UUID = "comp123" + cr.AbstractInstanciatedResource.UUID = cr.UUID + result := cr.ConvertToPricedResource(tools.COMPUTE_RESOURCE, req) + assert.NotNil(t, result) + assert.IsType(t, &resources.PricedComputeResource{}, result) +} + +func TestComputeResourcePricingProfile_GetPrice_CPUs(t *testing.T) { + start := time.Now() + end := start.Add(1 * time.Hour) + profile := resources.ComputeResourcePricingProfile{ + CPUsPrices: map[string]float64{"Xeon": 2.0}, + ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{ + AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{ + Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0}, + }, + }, + } + + price, err := profile.GetPrice(2, 3600, start, end, "cpus", "Xeon") + require.NoError(t, err) + assert.Greater(t, price, float64(0)) +} + +func TestComputeResourcePricingProfile_GetPrice_InvalidParams(t *testing.T) { + profile := resources.ComputeResourcePricingProfile{} + _, err := profile.GetPrice(1, 3600, time.Now(), time.Now()) + assert.Error(t, err) + assert.Equal(t, "params must be set", err.Error()) +} + +func TestPricedComputeResource_GetPrice(t *testing.T) { + start := time.Now() + end := start.Add(1 * time.Hour) + profile := &resources.ComputeResourcePricingProfile{ + CPUsPrices: map[string]float64{"Xeon": 1.0}, + GPUsPrices: map[string]float64{"Tesla": 2.0}, + RAMPrice: 0.5, + ExploitPricingProfile: pricing.ExploitPricingProfile[pricing.TimePricingStrategy]{ + AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{ + Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{Price: 1.0}, + }, + }, + } + r := resources.PricedComputeResource{ + PricedResource: resources.PricedResource{ + ResourceID: "comp456", + PricingProfiles: []pricing.PricingProfileITF{profile}, + UsageStart: &start, + UsageEnd: &end, + ExplicitBookingDurationS: 3600, + }, + CPUsLocated: map[string]float64{"Xeon": 2}, + GPUsLocated: map[string]float64{"Tesla": 1}, + RAMLocated: 4, + } + + price, err := r.GetPrice() + require.NoError(t, err) + assert.Greater(t, price, float64(0)) +} + +func TestPricedComputeResource_GetPrice_MissingProfile(t *testing.T) { + r := resources.PricedComputeResource{ + PricedResource: resources.PricedResource{ + ResourceID: "comp789", + }, + } + _, err := r.GetPrice() + require.Error(t, err) + assert.Contains(t, err.Error(), "pricing profile must be set") +} + +func TestPricedComputeResource_FillWithDefaultProcessingUsage(t *testing.T) { + usage := &resources.ProcessingUsage{ + CPUs: map[string]*models.CPU{"t": {Model: "Xeon", Cores: 4}}, + GPUs: map[string]*models.GPU{"t1": {Model: "Tesla"}}, + RAM: &models.RAM{SizeGb: 16}, + } + r := &resources.PricedComputeResource{ + CPUsLocated: make(map[string]float64), + GPUsLocated: make(map[string]float64), + RAMLocated: 0, + } + r.FillWithDefaultProcessingUsage(usage) + assert.Equal(t, float64(4), r.CPUsLocated["Xeon"]) + assert.Equal(t, float64(1), r.GPUsLocated["Tesla"]) + assert.Equal(t, float64(16), r.RAMLocated) +} diff --git a/models/resources/tests/data_test.go b/models/resources/tests/data_test.go new file mode 100644 index 0000000..bee639c --- /dev/null +++ b/models/resources/tests/data_test.go @@ -0,0 +1,125 @@ +package resources_test + +import ( + "testing" + "time" + + "cloud.o-forge.io/core/oc-lib/models/common/models" + "cloud.o-forge.io/core/oc-lib/models/common/pricing" + "cloud.o-forge.io/core/oc-lib/models/resources" + "cloud.o-forge.io/core/oc-lib/tools" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDataResource_GetType(t *testing.T) { + d := &resources.DataResource{} + assert.Equal(t, tools.DATA_RESOURCE.String(), d.GetType()) +} + +func TestDataResource_GetAccessor(t *testing.T) { + req := &tools.APIRequest{} + acc := (&resources.DataResource{}).GetAccessor(req) + assert.NotNil(t, acc) +} + +func TestDataResource_ConvertToPricedResource(t *testing.T) { + d := &resources.DataResource{} + d.UUID = "123" + res := d.ConvertToPricedResource(tools.DATA_RESOURCE, &tools.APIRequest{}) + assert.IsType(t, &resources.PricedDataResource{}, res) + + nilRes := d.ConvertToPricedResource(tools.PROCESSING_RESOURCE, &tools.APIRequest{}) + assert.Nil(t, nilRes) +} + +func TestDataInstance_StoreDraftDefault(t *testing.T) { + di := &resources.DataInstance{ + Source: "test-src", + ResourceInstance: resources.ResourceInstance[*resources.DataResourcePartnership]{ + Env: []models.Param{}, + }, + } + di.StoreDraftDefault() + assert.Len(t, di.ResourceInstance.Env, 1) + assert.Equal(t, "source", di.ResourceInstance.Env[0].Attr) + assert.Equal(t, "test-src", di.ResourceInstance.Env[0].Value) + + // Call again, should not duplicate + di.StoreDraftDefault() + assert.Len(t, di.ResourceInstance.Env, 1) +} + +func TestDataResourcePricingStrategy_GetQuantity(t *testing.T) { + tests := []struct { + strategy resources.DataResourcePricingStrategy + input float64 + expected float64 + }{ + {resources.PER_DOWNLOAD, 1, 1}, + {resources.PER_TB_DOWNLOADED, 1, 1000}, + {resources.PER_GB_DOWNLOADED, 2.5, 2.5}, + {resources.PER_MB_DOWNLOADED, 1, 0.001}, + {resources.PER_KB_DOWNLOADED, 1, 0.000001}, + } + + for _, tt := range tests { + q, err := tt.strategy.GetQuantity(tt.input) + require.NoError(t, err) + assert.InDelta(t, tt.expected, q, 1e-9) + } + + _, err := resources.DataResourcePricingStrategy(999).GetQuantity(1) + assert.Error(t, err) +} + +func TestDataResourcePricingProfile_IsPurchased(t *testing.T) { + profile := &resources.DataResourcePricingProfile{} + profile.Pricing.BuyingStrategy = pricing.PAY_PER_USE + assert.False(t, profile.IsPurchased()) + + profile.Pricing.BuyingStrategy = pricing.SUBSCRIPTION + assert.True(t, profile.IsPurchased()) +} + +func TestPricedDataResource_GetPrice(t *testing.T) { + now := time.Now() + later := now.Add(1 * time.Hour) + mockPrice := 42.0 + + pricingProfile := &resources.DataResourcePricingProfile{AccessPricingProfile: pricing.AccessPricingProfile[resources.DataResourcePricingStrategy]{ + Pricing: pricing.PricingStrategy[resources.DataResourcePricingStrategy]{Price: 42.0}}, + } + pricingProfile.Pricing.OverrideStrategy = resources.PER_GB_DOWNLOADED + + r := &resources.PricedDataResource{ + PricedResource: resources.PricedResource{ + UsageStart: &now, + UsageEnd: &later, + PricingProfiles: []pricing.PricingProfileITF{ + pricingProfile, + }, + }, + } + + price, err := r.GetPrice() + require.NoError(t, err) + assert.Equal(t, mockPrice, price) +} + +func TestPricedDataResource_GetPrice_NoProfiles(t *testing.T) { + r := &resources.PricedDataResource{ + PricedResource: resources.PricedResource{ + ResourceID: "test-resource", + }, + } + _, err := r.GetPrice() + assert.Error(t, err) + assert.Contains(t, err.Error(), "pricing profile must be set") +} + +func TestPricedDataResource_GetType(t *testing.T) { + r := &resources.PricedDataResource{} + assert.Equal(t, tools.DATA_RESOURCE, r.GetType()) +} diff --git a/models/resources/tests/priced_resource_test.go b/models/resources/tests/priced_resource_test.go new file mode 100644 index 0000000..61616a8 --- /dev/null +++ b/models/resources/tests/priced_resource_test.go @@ -0,0 +1,142 @@ +package resources_test + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cloud.o-forge.io/core/oc-lib/models/common/pricing" + "cloud.o-forge.io/core/oc-lib/models/resources" + "cloud.o-forge.io/core/oc-lib/tools" +) + +// ---- Mock PricingProfile ---- + +type MockPricingProfile struct { + pricing.PricingProfileITF + Purchased bool + ReturnErr bool + ReturnCost float64 +} + +func (m *MockPricingProfile) IsPurchased() bool { + return m.Purchased +} + +func (m *MockPricingProfile) GetPrice(amount float64, explicitDuration float64, start time.Time, end time.Time, _ ...string) (float64, error) { + if m.ReturnErr { + return 0, errors.New("mock error") + } + return m.ReturnCost, nil +} + +// ---- Tests ---- + +func TestGetIDAndCreatorAndType(t *testing.T) { + r := resources.PricedResource{ + ResourceID: "res-123", + CreatorID: "user-abc", + ResourceType: tools.DATA_RESOURCE, + } + assert.Equal(t, "res-123", r.GetID()) + assert.Equal(t, "user-abc", r.GetCreatorID()) + assert.Equal(t, tools.DATA_RESOURCE, r.GetType()) +} + +func TestIsPurchased(t *testing.T) { + t.Run("nil selected pricing returns false", func(t *testing.T) { + r := &resources.PricedResource{} + assert.False(t, r.IsPurchased()) + }) + + t.Run("returns true if pricing profile is purchased", func(t *testing.T) { + mock := &MockPricingProfile{Purchased: true} + r := &resources.PricedResource{SelectedPricing: mock} + assert.True(t, r.IsPurchased()) + }) +} + +func TestGetAndSetLocationStartEnd(t *testing.T) { + r := &resources.PricedResource{} + + now := time.Now() + r.SetLocationStart(now) + r.SetLocationEnd(now.Add(2 * time.Hour)) + + assert.Equal(t, now, *r.GetLocationStart()) + assert.Equal(t, now.Add(2*time.Hour), *r.GetLocationEnd()) +} + +func TestGetExplicitDurationInS(t *testing.T) { + t.Run("uses explicit duration if set", func(t *testing.T) { + r := &resources.PricedResource{ExplicitBookingDurationS: 3600} + assert.Equal(t, 3600.0, r.GetExplicitDurationInS()) + }) + + t.Run("computes duration from start and end", func(t *testing.T) { + start := time.Now() + end := start.Add(2 * time.Hour) + r := &resources.PricedResource{UsageStart: &start, UsageEnd: &end} + assert.InDelta(t, 7200.0, r.GetExplicitDurationInS(), 0.1) + }) + + t.Run("defaults to 1 hour when times not set", func(t *testing.T) { + r := &resources.PricedResource{} + assert.InDelta(t, 3600.0, r.GetExplicitDurationInS(), 0.1) + }) +} + +func TestGetPrice(t *testing.T) { + t.Run("returns error if no pricing profile", func(t *testing.T) { + r := &resources.PricedResource{ResourceID: "no-profile"} + price, err := r.GetPrice() + require.Error(t, err) + assert.Contains(t, err.Error(), "pricing profile must be set") + assert.Equal(t, 0.0, price) + }) + + t.Run("uses first profile if selected is nil", func(t *testing.T) { + start := time.Now() + end := start.Add(30 * time.Minute) + mock := &MockPricingProfile{ReturnCost: 42.0} + r := &resources.PricedResource{ + PricingProfiles: []pricing.PricingProfileITF{mock}, + UsageStart: &start, + UsageEnd: &end, + } + price, err := r.GetPrice() + require.NoError(t, err) + assert.Equal(t, 42.0, price) + }) + + t.Run("returns error if profile GetPrice fails", func(t *testing.T) { + start := time.Now() + end := start.Add(1 * time.Hour) + mock := &MockPricingProfile{ReturnErr: true} + r := &resources.PricedResource{ + SelectedPricing: mock, + UsageStart: &start, + UsageEnd: &end, + } + price, err := r.GetPrice() + require.Error(t, err) + assert.Equal(t, 0.0, price) + }) + + t.Run("uses SelectedPricing if set", func(t *testing.T) { + start := time.Now() + end := start.Add(1 * time.Hour) + mock := &MockPricingProfile{ReturnCost: 10.0} + r := &resources.PricedResource{ + SelectedPricing: mock, + UsageStart: &start, + UsageEnd: &end, + } + price, err := r.GetPrice() + require.NoError(t, err) + assert.Equal(t, 10.0, price) + }) +} diff --git a/models/resources/tests/processing_test.go b/models/resources/tests/processing_test.go new file mode 100644 index 0000000..06e12c1 --- /dev/null +++ b/models/resources/tests/processing_test.go @@ -0,0 +1,115 @@ +package resources_test + +import ( + "testing" + "time" + + "cloud.o-forge.io/core/oc-lib/models/common/pricing" + . "cloud.o-forge.io/core/oc-lib/models/resources" + "cloud.o-forge.io/core/oc-lib/tools" + "github.com/stretchr/testify/assert" +) + +func TestProcessingResource_GetType(t *testing.T) { + r := &ProcessingResource{} + assert.Equal(t, tools.PROCESSING_RESOURCE.String(), r.GetType()) +} + +func TestPricedProcessingResource_GetType(t *testing.T) { + r := &PricedProcessingResource{} + assert.Equal(t, tools.PROCESSING_RESOURCE, r.GetType()) +} + +func TestPricedProcessingResource_GetExplicitDurationInS(t *testing.T) { + now := time.Now() + after := now.Add(2 * time.Hour) + + tests := []struct { + name string + input PricedProcessingResource + expected float64 + }{ + { + name: "Service without explicit duration", + input: PricedProcessingResource{ + IsService: true, + }, + expected: -1, + }, + { + name: "Nil start time, non-service", + input: PricedProcessingResource{ + PricedResource: PricedResource{ + UsageStart: nil, + }, + }, + expected: float64((1 * time.Hour).Seconds()), + }, + { + name: "Duration computed from start and end", + input: PricedProcessingResource{ + PricedResource: PricedResource{ + UsageStart: &now, + UsageEnd: &after, + }, + }, + expected: float64((2 * time.Hour).Seconds()), + }, + { + name: "Explicit duration takes precedence", + input: PricedProcessingResource{ + PricedResource: PricedResource{ + ExplicitBookingDurationS: 1337, + }, + }, + expected: 1337, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, test.input.GetExplicitDurationInS()) + }) + } +} + +func TestProcessingResource_GetAccessor(t *testing.T) { + request := &tools.APIRequest{} + r := &ProcessingResource{} + acc := r.GetAccessor(request) + assert.NotNil(t, acc) +} + +func TestProcessingResourcePricingProfile_GetPrice(t *testing.T) { + start := time.Now() + end := start.Add(2 * time.Hour) + mockPricing := pricing.AccessPricingProfile[pricing.TimePricingStrategy]{ + Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{ + Price: 100.0, + }, + } + profile := &ProcessingResourcePricingProfile{mockPricing} + price, err := profile.GetPrice(0, 0, start, end) + assert.NoError(t, err) + assert.Equal(t, 100.0, price) +} + +func TestProcessingResourcePricingProfile_IsPurchased(t *testing.T) { + nonPurchased := &ProcessingResourcePricingProfile{ + AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{ + Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{ + BuyingStrategy: pricing.PAY_PER_USE, + }, + }, + } + assert.False(t, nonPurchased.IsPurchased()) + + purchased := &ProcessingResourcePricingProfile{ + AccessPricingProfile: pricing.AccessPricingProfile[pricing.TimePricingStrategy]{ + Pricing: pricing.PricingStrategy[pricing.TimePricingStrategy]{ + BuyingStrategy: pricing.UNLIMITED, + }, + }, + } + assert.True(t, purchased.IsPurchased()) +} diff --git a/models/resources/tests/resource_test.go b/models/resources/tests/resource_test.go new file mode 100644 index 0000000..d056713 --- /dev/null +++ b/models/resources/tests/resource_test.go @@ -0,0 +1,106 @@ +package resources_test + +import ( + "testing" + + "cloud.o-forge.io/core/oc-lib/models/common/pricing" + "cloud.o-forge.io/core/oc-lib/models/resources" + "cloud.o-forge.io/core/oc-lib/models/utils" + "cloud.o-forge.io/core/oc-lib/tools" + "github.com/stretchr/testify/assert" +) + +type MockInstance struct { + ID string + Name string + resources.ResourceInstance[*MockPartner] +} + +func (m *MockInstance) GetID() string { return m.ID } +func (m *MockInstance) GetName() string { return m.Name } +func (m *MockInstance) ClearEnv() {} +func (m *MockInstance) ClearPeerGroups() {} +func (m *MockInstance) GetPricingsProfiles(string, []string) []pricing.PricingProfileITF { return nil } +func (m *MockInstance) GetPeerGroups() ([]resources.ResourcePartnerITF, []map[string][]string) { + return nil, []map[string][]string{ + {"peer1": {"group1"}}, + } +} + +type MockPartner struct { + groups map[string][]string +} + +func (m *MockPartner) GetPeerGroups() map[string][]string { + return m.groups +} +func (m *MockPartner) ClearPeerGroups() {} +func (m *MockPartner) GetPricingsProfiles(string, []string) []pricing.PricingProfileITF { + return nil +} + +type MockDBObject struct { + utils.AbstractObject + isDraft bool +} + +func (m *MockDBObject) IsDrafted() bool { + return m.isDraft +} + +func TestGetSelectedInstance_WithValidIndex(t *testing.T) { + index := 1 + inst1 := &MockInstance{ID: "1"} + inst2 := &MockInstance{ID: "2"} + resource := &resources.AbstractInstanciatedResource[*MockInstance]{ + AbstractResource: resources.AbstractResource{SelectedInstanceIndex: &index}, + Instances: []*MockInstance{inst1, inst2}, + } + result := resource.GetSelectedInstance() + assert.Equal(t, inst2, result) +} + +func TestGetSelectedInstance_NoIndex(t *testing.T) { + inst := &MockInstance{ID: "1"} + resource := &resources.AbstractInstanciatedResource[*MockInstance]{ + Instances: []*MockInstance{inst}, + } + result := resource.GetSelectedInstance() + assert.Equal(t, inst, result) +} + +func TestCanUpdate_WhenOnlyStateDiffers(t *testing.T) { + resource := &resources.AbstractResource{AbstractObject: utils.AbstractObject{IsDraft: false}} + set := &MockDBObject{isDraft: true} + canUpdate, updated := resource.CanUpdate(set) + assert.True(t, canUpdate) + assert.Equal(t, set, updated) +} + +func TestVerifyAuthAction_WithMatchingGroup(t *testing.T) { + inst := &MockInstance{ + ResourceInstance: resources.ResourceInstance[*MockPartner]{ + Partnerships: []*MockPartner{ + {groups: map[string][]string{"peer1": {"group1"}}}, + }, + }, + } + req := &tools.APIRequest{PeerID: "peer1", Groups: []string{"group1"}} + result := resources.VerifyAuthAction([]*MockInstance{inst}, req) + assert.Len(t, result, 1) +} + +type FakeResource struct { + resources.AbstractInstanciatedResource[*MockInstance] +} + +func (f *FakeResource) Trim() {} +func (f *FakeResource) SetAllowedInstances(*tools.APIRequest) {} +func (f *FakeResource) VerifyAuth(*tools.APIRequest) bool { return true } + +func TestNewAccessor_ReturnsValid(t *testing.T) { + acc := resources.NewAccessor[*FakeResource](tools.COMPUTE_RESOURCE, &tools.APIRequest{}, func() utils.DBObject { + return &FakeResource{} + }) + assert.NotNil(t, acc) +} diff --git a/models/resources/tests/storage_test.go b/models/resources/tests/storage_test.go new file mode 100644 index 0000000..2967e06 --- /dev/null +++ b/models/resources/tests/storage_test.go @@ -0,0 +1,149 @@ +package resources_test + +import ( + "testing" + "time" + + "cloud.o-forge.io/core/oc-lib/models/common/models" + "cloud.o-forge.io/core/oc-lib/models/common/pricing" + "cloud.o-forge.io/core/oc-lib/tools" + "github.com/stretchr/testify/assert" + + "cloud.o-forge.io/core/oc-lib/models/resources" +) + +func TestStorageResource_GetType(t *testing.T) { + res := &resources.StorageResource{} + assert.Equal(t, tools.STORAGE_RESOURCE.String(), res.GetType()) +} + +func TestStorageResource_GetAccessor(t *testing.T) { + res := &resources.StorageResource{} + req := &tools.APIRequest{} + accessor := res.GetAccessor(req) + assert.NotNil(t, accessor) +} + +func TestStorageResource_ConvertToPricedResource_ValidType(t *testing.T) { + res := &resources.StorageResource{} + res.AbstractInstanciatedResource.CreatorID = "creator" + res.AbstractInstanciatedResource.UUID = "res-id" + priced := res.ConvertToPricedResource(tools.STORAGE_RESOURCE, &tools.APIRequest{}) + assert.NotNil(t, priced) + assert.IsType(t, &resources.PricedStorageResource{}, priced) +} + +func TestStorageResource_ConvertToPricedResource_InvalidType(t *testing.T) { + res := &resources.StorageResource{} + priced := res.ConvertToPricedResource(tools.COMPUTE_RESOURCE, &tools.APIRequest{}) + assert.Nil(t, priced) +} + +func TestStorageResourceInstance_ClearEnv(t *testing.T) { + inst := &resources.StorageResourceInstance{ + Credentials: &resources.Credentials{Login: "test"}, + ResourceInstance: resources.ResourceInstance[*resources.StorageResourcePartnership]{ + Env: []models.Param{{Attr: "A"}}, + Inputs: []models.Param{{Attr: "B"}}, + Outputs: []models.Param{{Attr: "C"}}, + }, + } + + inst.ClearEnv() + assert.Nil(t, inst.Credentials) + assert.Empty(t, inst.Env) + assert.Empty(t, inst.Inputs) + assert.Empty(t, inst.Outputs) +} + +func TestStorageResourceInstance_StoreDraftDefault(t *testing.T) { + inst := &resources.StorageResourceInstance{ + Source: "my-source", + ResourceInstance: resources.ResourceInstance[*resources.StorageResourcePartnership]{ + Env: []models.Param{}, + }, + } + + inst.StoreDraftDefault() + assert.Len(t, inst.Env, 1) + assert.Equal(t, "source", inst.Env[0].Attr) + assert.Equal(t, "my-source", inst.Env[0].Value) + assert.True(t, inst.Env[0].Readonly) +} + +func TestStorageResourcePricingStrategy_GetQuantity(t *testing.T) { + tests := []struct { + strategy resources.StorageResourcePricingStrategy + dataGB float64 + expect float64 + }{ + {resources.PER_DATA_STORED, 1.2, 1.2}, + {resources.PER_TB_STORED, 1.2, 1200}, + {resources.PER_GB_STORED, 2.5, 2.5}, + {resources.PER_MB_STORED, 1.0, 1000}, + {resources.PER_KB_STORED, 0.1, 100000}, + } + + for _, tt := range tests { + q, err := tt.strategy.GetQuantity(tt.dataGB) + assert.NoError(t, err) + assert.Equal(t, tt.expect, q) + } +} + +func TestStorageResourcePricingStrategy_GetQuantity_Invalid(t *testing.T) { + invalid := resources.StorageResourcePricingStrategy(99) + q, err := invalid.GetQuantity(1.0) + assert.Error(t, err) + assert.Equal(t, 0.0, q) +} + +func TestPricedStorageResource_GetPrice_NoProfiles(t *testing.T) { + res := &resources.PricedStorageResource{ + PricedResource: resources.PricedResource{ + ResourceID: "res-id", + }, + } + _, err := res.GetPrice() + assert.Error(t, err) +} + +func TestPricedStorageResource_GetPrice_WithPricing(t *testing.T) { + now := time.Now() + end := now.Add(2 * time.Hour) + profile := &resources.StorageResourcePricingProfile{ + ExploitPricingProfile: pricing.ExploitPricingProfile[resources.StorageResourcePricingStrategy]{ + AccessPricingProfile: pricing.AccessPricingProfile[resources.StorageResourcePricingStrategy]{ + Pricing: pricing.PricingStrategy[resources.StorageResourcePricingStrategy]{ + BuyingStrategy: pricing.PAY_PER_USE, + Price: 42.0, + }, + }, + }, + } + res := &resources.PricedStorageResource{ + PricedResource: resources.PricedResource{ + UsageStart: &now, + UsageEnd: &end, + PricingProfiles: []pricing.PricingProfileITF{profile}, + }, + UsageStorageGB: 1.0, + } + price, err := res.GetPrice() + assert.NoError(t, err) + assert.Equal(t, 42.0, price) +} + +func TestStorageResourcePricingProfile_IsPurchased(t *testing.T) { + p := &resources.StorageResourcePricingProfile{ + ExploitPricingProfile: pricing.ExploitPricingProfile[resources.StorageResourcePricingStrategy]{ + AccessPricingProfile: pricing.AccessPricingProfile[resources.StorageResourcePricingStrategy]{ + Pricing: pricing.PricingStrategy[resources.StorageResourcePricingStrategy]{BuyingStrategy: pricing.PAY_PER_USE}, + }, + }, + } + assert.False(t, p.IsPurchased()) + + p.Pricing.BuyingStrategy = pricing.UNLIMITED + assert.True(t, p.IsPurchased()) +} diff --git a/models/resources/tests/workflow_test.go b/models/resources/tests/workflow_test.go new file mode 100644 index 0000000..5d8eeb9 --- /dev/null +++ b/models/resources/tests/workflow_test.go @@ -0,0 +1,62 @@ +package resources_test + +import ( + "testing" + + "cloud.o-forge.io/core/oc-lib/models/utils" + "cloud.o-forge.io/core/oc-lib/tools" + "github.com/stretchr/testify/assert" + + "cloud.o-forge.io/core/oc-lib/models/resources" +) + +func TestWorkflowResource_GetType(t *testing.T) { + w := &resources.WorkflowResource{} + assert.Equal(t, tools.WORKFLOW_RESOURCE.String(), w.GetType()) +} + +func TestWorkflowResource_ConvertToPricedResource(t *testing.T) { + w := &resources.WorkflowResource{ + AbstractResource: resources.AbstractResource{ + AbstractObject: utils.AbstractObject{ + Name: "Test Workflow", + UUID: "workflow-uuid", + CreatorID: "creator-id", + }, + Logo: "logo.png", + }, + } + + req := &tools.APIRequest{ + PeerID: "peer-1", + Groups: []string{"group1"}, + } + + pr := w.ConvertToPricedResource(tools.WORKFLOW_RESOURCE, req) + assert.Equal(t, "creator-id", pr.GetCreatorID()) + assert.Equal(t, tools.WORKFLOW_RESOURCE, pr.GetType()) +} + +func TestWorkflowResource_ClearEnv(t *testing.T) { + w := &resources.WorkflowResource{} + assert.Equal(t, w, w.ClearEnv()) +} + +func TestWorkflowResource_Trim(t *testing.T) { + w := &resources.WorkflowResource{} + w.Trim() + // nothing to assert; just test that it doesn't panic +} + +func TestWorkflowResource_SetAllowedInstances(t *testing.T) { + w := &resources.WorkflowResource{} + w.SetAllowedInstances(&tools.APIRequest{}) + // no-op; just confirm no crash +} + +func TestWorkflowResource_GetAccessor(t *testing.T) { + w := &resources.WorkflowResource{} + request := &tools.APIRequest{} + accessor := w.GetAccessor(request) + assert.NotNil(t, accessor) +} diff --git a/tools/tests/remote_caller_test.go b/tools/tests/remote_caller_test.go new file mode 100644 index 0000000..46f0790 --- /dev/null +++ b/tools/tests/remote_caller_test.go @@ -0,0 +1,227 @@ +package tools + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "cloud.o-forge.io/core/oc-lib/tools" +) + +func TestMethodString(t *testing.T) { + tests := []struct { + method tools.METHOD + expected string + }{ + {tools.GET, "GET"}, + {tools.PUT, "PUT"}, + {tools.POST, "POST"}, + {tools.POSTCHECK, "POST"}, + {tools.DELETE, "DELETE"}, + {tools.STRICT_INTERNAL_GET, "INTERNALGET"}, + {tools.STRICT_INTERNAL_PUT, "INTERNALPUT"}, + {tools.STRICT_INTERNAL_POST, "INTERNALPOST"}, + {tools.STRICT_INTERNAL_DELETE, "INTERNALDELETE"}, + } + + for _, test := range tests { + if test.method.String() != test.expected { + t.Errorf("Expected %s, got %s", test.expected, test.method.String()) + } + } +} + +func TestToMethod(t *testing.T) { + method := tools.ToMethod("INTERNALPUT") + if method != tools.STRICT_INTERNAL_PUT { + t.Errorf("Expected STRICT_INTERNAL_PUT, got %v", method) + } + + defaultMethod := tools.ToMethod("INVALID") + if defaultMethod != tools.GET { + t.Errorf("Expected default GET, got %v", defaultMethod) + } +} + +func TestEnumIndex(t *testing.T) { + if tools.GET.EnumIndex() != 0 { + t.Errorf("Expected index 0 for GET, got %d", tools.GET.EnumIndex()) + } +} + +func TestCallGet(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`"ok"`)) + })) + defer ts.Close() + + caller := &tools.HTTPCaller{} + body, err := caller.CallGet(ts.URL, "/test", "application/json") + if err != nil || string(body) != `"ok"` { + t.Errorf("Expected body to be ok, got %s", string(body)) + } +} + +func TestCallPost(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(w, r.Body) + })) + defer ts.Close() + + caller := &tools.HTTPCaller{} + body, err := caller.CallPost(ts.URL, "/post", map[string]string{"key": "val"}) + if err != nil || !strings.Contains(string(body), "key") { + t.Errorf("POST failed, body: %s", string(body)) + } +} + +func TestCallPut(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(w, r.Body) + })) + defer ts.Close() + + caller := &tools.HTTPCaller{} + body, err := caller.CallPut(ts.URL, "/put", map[string]interface{}{"foo": "bar"}) + if err != nil || !strings.Contains(string(body), "foo") { + t.Errorf("PUT failed, body: %s", string(body)) + } +} + +func TestCallDelete(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`deleted`)) + })) + defer ts.Close() + + caller := &tools.HTTPCaller{} + body, err := caller.CallDelete(ts.URL, "/delete") + if err != nil || string(body) != "deleted" { + t.Errorf("DELETE failed, body: %s", string(body)) + } +} + +func TestCallRaw(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + caller := &tools.HTTPCaller{} + resp, err := caller.CallRaw("POST", ts.URL, "/", map[string]interface{}{"a": 1}, "application/json", true) + if err != nil || resp.StatusCode != http.StatusOK { + t.Errorf("CallRaw failed") + } +} + +func TestCallForm(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Expected POST, got %s", r.Method) + } + })) + defer ts.Close() + + caller := &tools.HTTPCaller{} + form := url.Values{} + form.Set("foo", "bar") + + _, err := caller.CallForm("POST", ts.URL, "/form", form, "application/x-www-form-urlencoded", true) + if err != nil { + t.Errorf("CallForm error: %v", err) + } +} + +func TestStoreResp(t *testing.T) { + resp := &http.Response{ + Header: http.Header{}, + StatusCode: 200, + Body: io.NopCloser(bytes.NewBuffer([]byte("body content"))), + } + + caller := &tools.HTTPCaller{} + err := caller.StoreResp(resp) + if err != nil { + t.Errorf("StoreResp failed: %v", err) + } + + if string(caller.LastResults["body"].([]byte)) != "body content" { + t.Errorf("Expected body content") + } +} + +func TestNewHTTPCaller(t *testing.T) { + c := tools.NewHTTPCaller(nil) + if c.Disabled != false { + t.Errorf("Expected Disabled false") + } +} + +func TestGetUrls(t *testing.T) { + urls := map[tools.DataType]map[tools.METHOD]string{} + c := tools.NewHTTPCaller(urls) + if c.GetUrls() == nil { + t.Errorf("GetUrls returned nil") + } +} + +func TestDeepCopy(t *testing.T) { + original := tools.NewHTTPCaller(nil) + copy := tools.HTTPCaller{} + err := original.DeepCopy(copy) + if err != nil { + t.Errorf("DeepCopy failed: %v", err) + } +} + +func TestCallPost_InvalidJSON(t *testing.T) { + caller := &tools.HTTPCaller{} + _, err := caller.CallPost("http://invalid", "/post", func() {}) + if err == nil { + t.Error("Expected error when marshaling unsupported type") + } +} + +func TestCallPut_ErrorOnNewRequest(t *testing.T) { + caller := &tools.HTTPCaller{} + _, err := caller.CallPut("http://[::1]:namedport", "/put", nil) + if err == nil { + t.Error("Expected error from invalid URL") + } +} + +func TestCallGet_Error(t *testing.T) { + caller := &tools.HTTPCaller{} + _, err := caller.CallGet("http://[::1]:namedport", "/bad", "application/json") + if err == nil { + t.Error("Expected error from invalid URL") + } +} + +func TestCallDelete_Error(t *testing.T) { + caller := &tools.HTTPCaller{} + _, err := caller.CallDelete("http://[::1]:namedport", "/bad") + if err == nil { + t.Error("Expected error from invalid URL") + } +} + +func TestCallRaw_Error(t *testing.T) { + caller := &tools.HTTPCaller{} + _, err := caller.CallRaw("POST", "http://[::1]:namedport", "/raw", nil, "application/json", false) + if err == nil { + t.Error("Expected error from invalid URL") + } +} + +func TestCallForm_Error(t *testing.T) { + caller := &tools.HTTPCaller{} + _, err := caller.CallForm("POST", "http://[::1]:namedport", "/form", url.Values{}, "application/json", false) + if err == nil { + t.Error("Expected error from invalid URL") + } +}