Compare commits

..

No commits in common. "main" and "namespace" have entirely different histories.

59 changed files with 181 additions and 3001 deletions

View File

@ -26,12 +26,12 @@ import (
func GetConfLoader() *onion.Onion { func GetConfLoader() *onion.Onion {
logger := zerolog.New(os.Stdout).With().Timestamp().Logger() logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
AppName := GetAppName() AppName := GetAppName()
EnvPrefix := "OC_" EnvPrefix := strings.ToUpper(AppName[0:2]+AppName[3:]) + "_"
defaultConfigFile := "/etc/oc/" + AppName[3:] + ".json" defaultConfigFile := "/etc/oc/" + AppName[3:] + ".json"
localConfigFile := "./" + AppName[3:] + ".json" localConfigFile := "./" + AppName[3:] + ".json"
var configFile string var configFile string
var o *onion.Onion var o *onion.Onion
l3 := GetEnvVarLayer(EnvPrefix) l3 := onion.NewEnvLayerPrefix("_", EnvPrefix)
l2, err := onion.NewFileLayer(localConfigFile, nil) l2, err := onion.NewFileLayer(localConfigFile, nil)
if err == nil { if err == nil {
logger.Info().Msg("Local config file found " + localConfigFile + ", overriding default file") logger.Info().Msg("Local config file found " + localConfigFile + ", overriding default file")
@ -54,17 +54,3 @@ func GetConfLoader() *onion.Onion {
} }
return o return o
} }
func GetEnvVarLayer(prefix string) onion.Layer {
envVars := make(map[string]interface{})
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
key := pair[0]
if strings.HasPrefix(key, prefix) {
envVars[strings.TrimPrefix(key, prefix)] = pair[1]
}
}
return onion.NewMapLayer(envVars)
}

View File

@ -287,7 +287,7 @@ func (m *MongoDB) Search(filters *dbs.Filters, collection_name string) (*mongo.C
return nil, 503, err return nil, 503, err
} }
opts := options.Find() opts := options.Find()
opts.SetLimit(1000) opts.SetLimit(100)
targetDBCollection := CollectionMap[collection_name] targetDBCollection := CollectionMap[collection_name]
orList := bson.A{} orList := bson.A{}
andList := bson.A{} andList := bson.A{}

View File

@ -17,7 +17,6 @@ import (
"cloud.o-forge.io/core/oc-lib/models" "cloud.o-forge.io/core/oc-lib/models"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area" "cloud.o-forge.io/core/oc-lib/models/collaborative_area"
"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule" "cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
"cloud.o-forge.io/core/oc-lib/models/compute_units"
"cloud.o-forge.io/core/oc-lib/models/order" "cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/resources" "cloud.o-forge.io/core/oc-lib/models/resources"
@ -52,7 +51,6 @@ const (
RULE = tools.RULE RULE = tools.RULE
BOOKING = tools.BOOKING BOOKING = tools.BOOKING
ORDER = tools.ORDER ORDER = tools.ORDER
COMPUTE_UNITS = tools.COMPUTE_UNITS
) )
// will turn into standards api hostnames // will turn into standards api hostnames
@ -578,10 +576,3 @@ func (l *LibData) ToOrder() *order.Order {
} }
return nil return nil
} }
func (l *LibData) ToComputeUnits() *compute_units.ComputeUnits {
if l.Data.GetAccessor(nil).GetType() == tools.COMPUTE_UNITS {
return l.Data.(*compute_units.ComputeUnits)
}
return nil
}

3
go.mod Executable file → Normal file
View File

@ -10,13 +10,12 @@ require (
github.com/nats-io/nats.go v1.37.0 github.com/nats-io/nats.go v1.37.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.9.0
) )
require ( require (
github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
) )
require ( require (

4
go.sum Executable file → Normal file
View File

@ -106,13 +106,9 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=

View File

@ -15,7 +15,7 @@ import (
*/ */
type Booking struct { type Booking struct {
utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name) utils.AbstractObject // AbstractObject contains the basic fields of an object (id, name)
ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty" validate:"required"` // ExecutionsID is the ID of the executions ExecutionsID string `json:"executions_id,omitempty" bson:"executions_id,omitempty"` // ExecutionsID is the ID of the executions
DestPeerID string `json:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer DestPeerID string `json:"dest_peer_id,omitempty"` // DestPeerID is the ID of the destination peer
WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow WorkflowID string `json:"workflow_id,omitempty" bson:"workflow_id,omitempty"` // WorkflowID is the ID of the workflow
ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"` ExecutionID string `json:"execution_id,omitempty" bson:"execution_id,omitempty" validate:"required"`

View File

@ -11,13 +11,13 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "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) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the BookingMongoAccessor // New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor { func NewAccessor(request *tools.APIRequest) *bookingMongoAccessor {
return &BookingMongoAccessor{ return &bookingMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.BOOKING.String()), // Create a logger with the data type
Request: request, Request: request,
@ -29,11 +29,11 @@ func NewAccessor(request *tools.APIRequest) *BookingMongoAccessor {
/* /*
* Nothing special here, just the basic CRUD operations * 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) 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 { if set.(*Booking).State == 0 {
return nil, 400, errors.New("state is required") return nil, 400, errors.New("state is required")
} }
@ -41,25 +41,23 @@ func (a *BookingMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.D
return utils.GenericUpdateOne(realSet, id, a, &Booking{}) 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) 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) 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) { return utils.GenericLoadOne[*Booking](id, func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now() if d.(*Booking).State == enum.DRAFT && time.Now().UTC().After(d.(*Booking).ExpectedStartDate) {
now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
return utils.GenericDeleteOne(d.GetID(), a) return utils.GenericDeleteOne(d.GetID(), a)
} }
if (d.(*Booking).ExpectedEndDate) == nil { if (d.(*Booking).ExpectedEndDate) == nil {
d.(*Booking).State = enum.FORGOTTEN d.(*Booking).State = enum.FORGOTTEN
utils.GenericRawUpdateOne(d, id, a) utils.GenericRawUpdateOne(d, id, a)
} else if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) { } else if d.(*Booking).State == enum.SCHEDULED && time.Now().UTC().After(*&d.(*Booking).ExpectedStartDate) {
d.(*Booking).State = enum.DELAYED d.(*Booking).State = enum.DELAYED
utils.GenericRawUpdateOne(d, id, a) utils.GenericRawUpdateOne(d, id, a)
} }
@ -67,23 +65,21 @@ func (a *BookingMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
}, a) }, 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) 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) 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 { return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now() if d.(*Booking).State == enum.DRAFT && time.Now().UTC().After(d.(*Booking).ExpectedStartDate) {
now = now.Add(time.Second * -60)
if d.(*Booking).State == enum.DRAFT && now.UTC().After(d.(*Booking).ExpectedStartDate) {
utils.GenericDeleteOne(d.GetID(), a) utils.GenericDeleteOne(d.GetID(), a)
return nil return nil
} }
if d.(*Booking).State == enum.SCHEDULED && now.UTC().After(d.(*Booking).ExpectedStartDate) { if d.(*Booking).State == enum.SCHEDULED && time.Now().UTC().After(*&d.(*Booking).ExpectedStartDate) {
d.(*Booking).State = enum.DELAYED d.(*Booking).State = enum.DELAYED
utils.GenericRawUpdateOne(d, d.GetID(), a) utils.GenericRawUpdateOne(d, d.GetID(), a)
} }

View File

@ -1,87 +0,0 @@
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)
}

0
models/common/pricing/interfaces.go Executable file → Normal file
View File

0
models/common/pricing/pricing_profile.go Executable file → Normal file
View File

0
models/common/pricing/pricing_strategy.go Executable file → Normal file
View File

View File

@ -1,129 +0,0 @@
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)
}

View File

@ -1,58 +0,0 @@
package compute_units
import (
"cloud.o-forge.io/core/oc-lib/models/common/enum"
"cloud.o-forge.io/core/oc-lib/models/common/models"
"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/biter777/countries"
)
/*
* ComputeUnits is a struct that represents a compute units in your datacenters
*/
type ComputeUnitsCerts struct {
Host string `json:"host,omitempty" bson:"host,omitempty"`
Port string `json:"port,omitempty" bson:"port,omitempty"`
// for now only Kubernetes
CAData string `json:"ca_data,omitempty" bson:"ca_data,omitempty"`
CertData string `json:"cert_data,omitempty" bson:"cert_data,omitempty"`
KeyData string `json:"key_data,omitempty" bson:"key_data,omitempty"`
}
// TODO in the future multiple type of certs depending of infra type
type ComputeUnits struct {
utils.AbstractObject
Certs ComputeUnitsCerts `json:"certs,omitempty" bson:"certs,omitempty"`
MonitorPath string `json:"monitor_path,omitempty" bson:"monitor_path,omitempty"`
Location resources.GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
Architecture string `json:"architecture,omitempty" bson:"architecture,omitempty"` // Architecture is the architecture
Infrastructure enum.InfrastructureType `json:"infrastructure" bson:"infrastructure" default:"-1"` // Infrastructure is the infrastructure
Source string `json:"source,omitempty" bson:"source,omitempty"` // Source is the source of the resource
SecurityLevel string `json:"security_level,omitempty" bson:"security_level,omitempty"`
PowerSources []string `json:"power_sources,omitempty" bson:"power_sources,omitempty"`
AnnualCO2Emissions float64 `json:"annual_co2_emissions,omitempty" bson:"co2_emissions,omitempty"`
CPUs map[string]*models.CPU `bson:"cpus,omitempty" json:"cpus,omitempty"` // CPUs is the list of CPUs key is model
GPUs map[string]*models.GPU `bson:"gpus,omitempty" json:"gpus,omitempty"` // GPUs is the list of GPUs key is model
Nodes []*resources.ComputeNode `json:"nodes,omitempty" bson:"nodes,omitempty"`
ResourceID string `json:"resource_id" bson:"resource_id"`
}
func (r *ComputeUnits) StoreDraftDefault() {
r.IsDraft = true
}
func (r *ComputeUnits) CanDelete() bool {
return r.IsDraft // only draft ComputeUnits can be deleted
}
func (d *ComputeUnits) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor
}

View File

@ -1,108 +0,0 @@
package compute_units
import (
"encoding/json"
"errors"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/logs"
"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"
)
type computeUnitsMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
}
// New creates a new instance of the computeUnitsMongoAccessor
func NewAccessor(request *tools.APIRequest) *computeUnitsMongoAccessor {
return &computeUnitsMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.COMPUTE_UNITS.String()), // Create a logger with the data type
Request: request,
Type: tools.COMPUTE_UNITS,
},
}
}
/*
* Nothing special here, just the basic CRUD operations
*/
func (a *computeUnitsMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return utils.GenericDeleteOne(id, a)
}
func (a *computeUnitsMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
// should verify if a source is existing...
return utils.GenericUpdateOne(set, id, a, &ComputeUnits{})
}
func (a *computeUnitsMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return utils.GenericStoreOne(data.(*ComputeUnits), a)
}
func (a *computeUnitsMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
// is a publisher... that become a resources.
if data.IsDrafted() {
return nil, 422, errors.New("can't publish a drafted compute units")
}
computeUnits := data.(*ComputeUnits)
if computeUnits.MonitorPath == "" || computeUnits.GetID() != "" {
return nil, 422, errors.New("publishing is only allowed is it can be monitored and be accessible")
}
if res, code, err := a.LoadOne(computeUnits.GetID()); err != nil {
return nil, code, err
} else {
computeUnits = res.(*ComputeUnits)
}
resAccess := resources.NewAccessor[*resources.ComputeResource](tools.COMPUTE_RESOURCE, a.Request, func() utils.DBObject { return &resources.ComputeResource{} })
var instance *resources.ComputeResourceInstance
b, _ := json.Marshal(computeUnits)
json.Unmarshal(b, instance)
if computeUnits.ResourceID != "" {
// TODO dependent of a existing resource
res, code, err := resAccess.LoadOne(computeUnits.ResourceID)
if err == nil {
return nil, code, err
}
existingComputeResource := res.(*resources.ComputeResource)
if existingComputeResource.Architecture != computeUnits.Architecture || existingComputeResource.Infrastructure != computeUnits.Infrastructure {
return nil, 422, errors.New("should be same architecture & infrastructure from the resource")
}
existingComputeResource.Instances = append(existingComputeResource.Instances, instance)
return resAccess.UpdateOne(existingComputeResource, existingComputeResource.UUID)
} else {
var r resources.ComputeResource
b, _ := json.Marshal(computeUnits)
json.Unmarshal(b, &r)
r.Instances = append(r.Instances, instance)
res, code, err := resAccess.StoreOne(&r)
if err != nil {
return nil, code, err
}
computeUnits.ResourceID = res.GetID()
return a.UpdateOne(computeUnits, computeUnits.GetID())
}
}
func (a *computeUnitsMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*ComputeUnits](id, func(d utils.DBObject) (utils.DBObject, int, error) {
return d, 200, nil
}, a)
}
func (a *computeUnitsMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*ComputeUnits](a.getExec(), isDraft, a)
}
func (a *computeUnitsMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*ComputeUnits](filters, search, (&ComputeUnits{}).GetObjectFilters(search), a.getExec(), isDraft, a)
}
func (a *computeUnitsMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject {
return d
}
}

View File

@ -2,7 +2,6 @@ package models
import ( import (
"cloud.o-forge.io/core/oc-lib/logs" "cloud.o-forge.io/core/oc-lib/logs"
"cloud.o-forge.io/core/oc-lib/models/compute_units"
"cloud.o-forge.io/core/oc-lib/models/order" "cloud.o-forge.io/core/oc-lib/models/order"
"cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource" "cloud.o-forge.io/core/oc-lib/models/resources/purchase_resource"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
@ -22,7 +21,7 @@ import (
This package contains the models used in the application This package contains the models used in the application
It's used to create the models dynamically It's used to create the models dynamically
*/ */
var ModelsCatalog = map[string]func() utils.DBObject{ var models = map[string]func() utils.DBObject{
tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} }, tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} },
tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} }, tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} },
tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} }, tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} },
@ -39,14 +38,13 @@ var ModelsCatalog = map[string]func() utils.DBObject{
tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} }, tools.WORKSPACE_HISTORY.String(): func() utils.DBObject { return &w3.WorkspaceHistory{} },
tools.ORDER.String(): func() utils.DBObject { return &order.Order{} }, tools.ORDER.String(): func() utils.DBObject { return &order.Order{} },
tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} }, tools.PURCHASE_RESOURCE.String(): func() utils.DBObject { return &purchase_resource.PurchaseResource{} },
tools.COMPUTE_UNITS.String(): func() utils.DBObject { return &compute_units.ComputeUnits{} },
} }
// Model returns the model object based on the model type // Model returns the model object based on the model type
func Model(model int) utils.DBObject { func Model(model int) utils.DBObject {
log := logs.GetLogger() log := logs.GetLogger()
if _, ok := ModelsCatalog[tools.FromInt(model)]; ok { if _, ok := models[tools.FromInt(model)]; ok {
return ModelsCatalog[tools.FromInt(model)]() return models[tools.FromInt(model)]()
} }
log.Error().Msg("Can't find model " + tools.FromInt(model) + ".") log.Error().Msg("Can't find model " + tools.FromInt(model) + ".")
return nil return nil
@ -55,7 +53,7 @@ func Model(model int) utils.DBObject {
// GetModelsNames returns the names of the models // GetModelsNames returns the names of the models
func GetModelsNames() []string { func GetModelsNames() []string {
names := []string{} names := []string{}
for name := range ModelsCatalog { for name := range models {
names = append(names, name) names = append(names, name)
} }
return names return names

View File

@ -133,7 +133,7 @@ func (o *Order) draftStoreFromModel(scheduler *workflow_execution.WorkflowSchedu
o.SubOrders[peerOrder.GetID()] = peerOrder o.SubOrders[peerOrder.GetID()] = peerOrder
} }
// search an order with same user name and same session id // search an order with same user name and same session id
err := o.SumUpBill(request) err := o.sumUpBill(request)
if err != nil { if err != nil {
return err return err
} }
@ -196,7 +196,7 @@ func (d *Order) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request) // Create a new instance of the accessor
} }
func (d *Order) SumUpBill(request *tools.APIRequest) error { func (d *Order) sumUpBill(request *tools.APIRequest) error {
for _, b := range d.SubOrders { for _, b := range d.SubOrders {
err := b.SumUpBill(request) err := b.SumUpBill(request)
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"regexp"
"strings" "strings"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
@ -28,19 +29,47 @@ type PeerCache struct {
} }
// urlFormat formats the URL of the peer with the data type API function // urlFormat formats the URL of the peer with the data type API function
func (p *PeerCache) urlFormat(hostUrl string, dt tools.DataType) string { func (p *PeerCache) urlFormat(url string, dt tools.DataType) string {
return hostUrl + "/" + strings.ReplaceAll(dt.API(), "oc-", "") // localhost is replaced by the local peer URL
// because localhost must collide on a web request security protocol
localhost := ""
if strings.Contains(url, "localhost") {
localhost = "localhost"
}
if strings.Contains(url, "127.0.0.1") {
localhost = "127.0.0.1"
}
if localhost != "" {
r := regexp.MustCompile("(" + localhost + ":[0-9]+)")
t := r.FindString(url)
if t != "" {
url = strings.Replace(url, t, dt.API()+":8080/oc", -1)
} else {
url = strings.ReplaceAll(url, localhost, dt.API()+":8080/oc")
}
} else {
url = url + "/" + dt.API()
}
return url
} }
// checkPeerStatus checks the status of a peer // checkPeerStatus checks the status of a peer
func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool) { func (p *PeerCache) checkPeerStatus(peerID string, appName string, caller *tools.HTTPCaller) (*Peer, bool) {
api := tools.API{} api := tools.API{}
access := NewShallowAccessor() access := NewShallowAccessor()
res, code, _ := access.LoadOne(peerID) // Load the peer from db res, code, _ := access.LoadOne(peerID) // Load the peer from db
if code != 200 { // no peer no party if code != 200 { // no peer no party
return nil, false return nil, false
} }
url := p.urlFormat(res.(*Peer).Url, tools.PEER) + "/status" // Format the URL methods := caller.URLS[tools.PEER] // Get the methods url of the peer
if methods == nil {
return res.(*Peer), false
}
meth := methods[tools.POST] // Get the POST method to check status
if meth == "" {
return res.(*Peer), false
}
url := p.urlFormat(res.(*Peer).Url, tools.PEER) + meth // Format the URL
state, services := api.CheckRemotePeer(url) state, services := api.CheckRemotePeer(url)
res.(*Peer).ServicesState = services // Update the services states of the peer res.(*Peer).ServicesState = services // Update the services states of the peer
access.UpdateOne(res, peerID) // Update the peer in the db access.UpdateOne(res, peerID) // Update the peer in the db
@ -48,49 +77,48 @@ func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool)
} }
// LaunchPeerExecution launches an execution on a peer // LaunchPeerExecution launches an execution on a peer
// The method contacts the path described by : peer.Url + datatype path (from enums) + replacement of id by dataID
func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string, func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string,
dt tools.DataType, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (*PeerExecution, error) { dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) {
fmt.Println("Launching peer execution on", caller.GetUrls(), dt, method) fmt.Println("Launching peer execution on", caller.URLS, dt, method)
methods := caller.GetUrls()[dt] // Get the methods url of the data type methods := caller.URLS[dt] // Get the methods url of the data type
if m, ok := methods[method]; !ok || m == "" { if m, ok := methods[method]; !ok || m == "" {
return nil, errors.New("Requested method " + method.String() + " not declared in HTTPCaller") return nil, errors.New("no path found")
} }
path := methods[method] // Get the path corresponding to the action we want to execute meth := methods[method] // Get the method url to execute
path = strings.ReplaceAll(path, ":id", dataID) // Replace the id in the path in case of a DELETE / UPDATE method (it's a standard naming in OC) meth = strings.ReplaceAll(meth, ":id", dataID) // Replace the id in the url in case of a DELETE / UPDATE method (it's a standard naming in OC)
url := "" url := ""
// Check the status of the peer // Check the status of the peer
if mypeer, ok := p.checkPeerStatus(peerID, dt.API()); !ok && mypeer != nil { if mypeer, ok := p.checkPeerStatus(peerID, dt.API(), caller); !ok && mypeer != nil {
// If the peer is not reachable, add the execution to the failed executions list // If the peer is not reachable, add the execution to the failed executions list
pexec := &PeerExecution{ pexec := &PeerExecution{
Method: method.String(), Method: method.String(),
Url: p.urlFormat((mypeer.Url), dt) + path, // the url is constitued of : host URL + resource path + action path (ex : mypeer.com/datacenter/resourcetype/path/to/action) Url: p.urlFormat((mypeer.Url)+meth, dt),
Body: body, Body: body,
DataType: dt.EnumIndex(), DataType: dt.EnumIndex(),
DataID: dataID, DataID: dataID,
} }
mypeer.AddExecution(*pexec) mypeer.AddExecution(*pexec)
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
return nil, errors.New("peer is " + peerID + " not reachable") return nil, errors.New("peer is not reachable")
} else { } else {
if mypeer == nil { if mypeer == nil {
return nil, errors.New("peer " + peerID + " not found") return nil, errors.New("peer not found")
} }
// If the peer is reachable, launch the execution // If the peer is reachable, launch the execution
url = p.urlFormat((mypeer.Url), dt) + path // Format the URL url = p.urlFormat((mypeer.Url)+meth, dt) // Format the URL
tmp := mypeer.FailedExecution // Get the failed executions list tmp := mypeer.FailedExecution // Get the failed executions list
mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list
NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db
for _, v := range tmp { // Retry the failed executions for _, v := range tmp { // Retry the failed executions
go p.Exec(v.Url, tools.ToMethod(v.Method), v.Body, caller) go p.exec(v.Url, tools.ToMethod(v.Method), v.Body, caller)
} }
} }
return nil, p.Exec(url, method, body, caller) // Execute the method return nil, p.exec(url, method, body, caller) // Execute the method
} }
// exec executes the method on the peer // exec executes the method on the peer
func (p *PeerCache) Exec(url string, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) error { func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) error {
var b []byte var b []byte
var err error var err error
if method == tools.POST { // Execute the POST method if it's a POST method if method == tools.POST { // Execute the POST method if it's a POST method

View File

@ -11,13 +11,13 @@ import (
type peerMongoAccessor struct { type peerMongoAccessor struct {
utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
OverrideAuth bool overrideAuth bool
} }
// New creates a new instance of the peerMongoAccessor // New creates a new instance of the peerMongoAccessor
func NewShallowAccessor() *peerMongoAccessor { func NewShallowAccessor() *peerMongoAccessor {
return &peerMongoAccessor{ return &peerMongoAccessor{
OverrideAuth: true, overrideAuth: true,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Type: tools.PEER, Type: tools.PEER,
@ -27,7 +27,7 @@ func NewShallowAccessor() *peerMongoAccessor {
func NewAccessor(request *tools.APIRequest) *peerMongoAccessor { func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
return &peerMongoAccessor{ return &peerMongoAccessor{
OverrideAuth: false, overrideAuth: false,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type
Request: request, Request: request,
@ -37,7 +37,7 @@ func NewAccessor(request *tools.APIRequest) *peerMongoAccessor {
} }
func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool { func (wfa *peerMongoAccessor) ShouldVerifyAuth() bool {
return !wfa.OverrideAuth return !wfa.overrideAuth
} }
/* /*
@ -73,12 +73,12 @@ func (wfa *peerMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, in
} }
func (wfa *peerMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { func (wfa *peerMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*Peer](filters, search, wfa.GetDefaultFilter(search), return utils.GenericSearch[*Peer](filters, search, wfa.getDefaultFilter(search),
func(d utils.DBObject) utils.ShallowDBObject { func(d utils.DBObject) utils.ShallowDBObject {
return d return d
}, isDraft, wfa) }, isDraft, wfa)
} }
func (a *peerMongoAccessor) GetDefaultFilter(search string) *dbs.Filters { func (a *peerMongoAccessor) getDefaultFilter(search string) *dbs.Filters {
if i, err := strconv.Atoi(search); err == nil { if i, err := strconv.Atoi(search); err == nil {
return &dbs.Filters{ return &dbs.Filters{
Or: map[string][]dbs.Filter{ // search by name if no filters are provided Or: map[string][]dbs.Filter{ // search by name if no filters are provided

View File

@ -1,100 +0,0 @@
package peer_test
import (
"encoding/json"
"testing"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockHTTPCaller mocks tools.HTTPCaller
type MockHTTPCaller struct {
mock.Mock
URLS map[tools.DataType]map[tools.METHOD]string
}
func (c *MockHTTPCaller) GetUrls() map[tools.DataType]map[tools.METHOD]string {
return c.URLS
}
func (m *MockHTTPCaller) CallPost(url, token string, body interface{}, types ...string) ([]byte, error) {
args := m.Called(url, token, body)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPCaller) CallGet(url, token string, types ...string) ([]byte, error) {
args := m.Called(url, token)
return args.Get(0).([]byte), args.Error(1)
}
func (m *MockHTTPCaller) CallDelete(url, token string) ([]byte, error) {
args := m.Called(url, token)
return args.Get(0).([]byte), args.Error(1)
}
func TestLaunchPeerExecution_PeerNotReachable(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{
URLS: map[tools.DataType]map[tools.METHOD]string{
tools.PEER: {
tools.POST: "/execute/:id",
},
},
}
exec, err := cache.LaunchPeerExecution("peer-id", "data-id", tools.PEER, tools.POST, map[string]string{"a": "b"}, caller)
assert.Nil(t, exec)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not reachable")
}
func TestExecSuccess(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
response := map[string]interface{}{"result": "ok"}
data, _ := json.Marshal(response)
caller.On("CallPost", url, "", mock.Anything).Return(data, nil)
err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.NoError(t, err)
caller.AssertExpectations(t)
}
func TestExecReturnsErrorField(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
response := map[string]interface{}{"error": "something failed"}
data, _ := json.Marshal(response)
caller.On("CallPost", url, "", mock.Anything).Return(data, nil)
err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.Error(t, err)
assert.Equal(t, "something failed", err.Error())
}
func TestExecInvalidJSON(t *testing.T) {
cache := &peer.PeerCache{}
caller := &MockHTTPCaller{}
url := "http://mockpeer/resource"
caller.On("CallPost", url, "", mock.Anything).Return([]byte("{invalid json}"), nil)
err := cache.Exec(url, tools.POST, map[string]string{"key": "value"}, caller)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid character")
}
type mockAccessor struct {
loadOne func(string) (interface{}, int, error)
updateOne func(interface{}, string) error
}
func (m *mockAccessor) LoadOne(id string) (interface{}, int, error) {
return m.loadOne(id)
}
func (m *mockAccessor) UpdateOne(i interface{}, id string) error {
return m.updateOne(i, id)
}

View File

@ -1,127 +0,0 @@
package peer_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/utils"
)
type MockAccessor struct {
mock.Mock
utils.AbstractAccessor
}
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func newTestPeer() *peer.Peer {
return &peer.Peer{
Url: "http://localhost",
WalletAddress: "0x123",
PublicKey: "pubkey",
State: peer.SELF,
}
}
func TestDeleteOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("DeleteOne", "id").Return(newTestPeer(), 200, nil)
obj, code, err := mockAcc.DeleteOne("id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, obj)
mockAcc.AssertExpectations(t)
}
func TestUpdateOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
peerObj := newTestPeer()
mockAcc.On("UpdateOne", peerObj, "id").Return(peerObj, 200, nil)
obj, code, err := mockAcc.UpdateOne(peerObj, "id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, peerObj, obj)
mockAcc.AssertExpectations(t)
}
func TestStoreOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
peerObj := newTestPeer()
mockAcc.On("StoreOne", peerObj).Return(peerObj, 200, nil)
obj, code, err := mockAcc.StoreOne(peerObj)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, peerObj, obj)
mockAcc.AssertExpectations(t)
}
func TestLoadOne_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("LoadOne", "test-id").Return(newTestPeer(), 200, nil)
obj, code, err := mockAcc.LoadOne("test-id")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, obj)
mockAcc.AssertExpectations(t)
}
func TestLoadAll_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
expected := []utils.ShallowDBObject{newTestPeer()}
mockAcc.On("LoadAll", false).Return(expected, 200, nil)
objs, code, err := mockAcc.LoadAll(false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, expected, objs)
mockAcc.AssertExpectations(t)
}
func TestSearch_UsingMock(t *testing.T) {
mockAcc := new(MockAccessor)
filters := &dbs.Filters{}
expected := []utils.ShallowDBObject{newTestPeer()}
mockAcc.On("Search", filters, "test", false).Return(expected, 200, nil)
objs, code, err := mockAcc.Search(filters, "test", false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, expected, objs)
mockAcc.AssertExpectations(t)
}

10
models/resources/compute.go Executable file → Normal file
View File

@ -63,11 +63,7 @@ type ComputeResourceInstance struct {
type ComputeResourcePartnership struct { type ComputeResourcePartnership struct {
ResourcePartnerShip[*ComputeResourcePricingProfile] ResourcePartnerShip[*ComputeResourcePricingProfile]
MinGaranteedCPUsCores map[string]float64 `json:"garanteed_cpus,omitempty" bson:"garanteed_cpus,omitempty"` MaxAllowedCPUsCores map[string]int `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MinGaranteedGPUsMemoryGB map[string]float64 `json:"garanteed_gpus,omitempty" bson:"garanteed_gpus,omitempty"`
MinGaranteedRAMSize float64 `json:"garanteed_ram,omitempty" bson:"garanteed_ram,omitempty"`
MaxAllowedCPUsCores map[string]float64 `json:"allowed_cpus,omitempty" bson:"allowed_cpus,omitempty"`
MaxAllowedGPUsMemoryGB map[string]float64 `json:"allowed_gpus,omitempty" bson:"allowed_gpus,omitempty"` MaxAllowedGPUsMemoryGB map[string]float64 `json:"allowed_gpus,omitempty" bson:"allowed_gpus,omitempty"`
MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"` MaxAllowedRAMSize float64 `json:"allowed_ram,omitempty" bson:"allowed_ram,omitempty"`
} }
@ -155,9 +151,9 @@ func (r *PricedComputeResource) GetPrice() (float64, error) {
if len(r.PricingProfiles) == 0 { if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Compute" + r.ResourceID) 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) price := float64(0)
for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} { for _, l := range []map[string]float64{r.CPUsLocated, r.GPUsLocated} {
for model, amountOfData := range l { for model, amountOfData := range l {

4
models/resources/data.go Executable file → Normal file
View File

@ -164,9 +164,9 @@ func (r *PricedDataResource) GetPrice() (float64, error) {
if len(r.PricingProfiles) == 0 { if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Data" + r.ResourceID) 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 var err error
amountOfData := float64(1) amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 { if pricing.GetOverrideStrategyValue() >= 0 {

2
models/resources/interfaces.go Executable file → Normal file
View File

@ -25,8 +25,6 @@ type ResourceInstanceITF interface {
GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF
GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string) GetPeerGroups() ([]ResourcePartnerITF, []map[string][]string)
ClearPeerGroups() ClearPeerGroups()
GetSelectedPartnership() ResourcePartnerITF
GetPartnerships(peerID string, groups []string) []ResourcePartnerITF
} }
type ResourcePartnerITF interface { type ResourcePartnerITF interface {

0
models/resources/models.go Executable file → Normal file
View File

8
models/resources/priced_resource.go Executable file → Normal file
View File

@ -14,7 +14,7 @@ type PricedResource struct {
Logo string `json:"logo,omitempty" bson:"logo,omitempty"` Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"` InstancesRefs map[string]string `json:"instances_refs,omitempty" bson:"instances_refs,omitempty"`
PricingProfiles []pricing.PricingProfileITF `json:"pricing_profiles,omitempty" bson:"pricing_profiles,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"` ExplicitBookingDurationS float64 `json:"explicit_location_duration_s,omitempty" bson:"explicit_location_duration_s,omitempty"`
UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"` UsageStart *time.Time `json:"start,omitempty" bson:"start,omitempty"`
UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"` UsageEnd *time.Time `json:"end,omitempty" bson:"end,omitempty"`
@ -39,7 +39,7 @@ func (abs *PricedResource) IsPurchased() bool {
if abs.SelectedPricing == nil { if abs.SelectedPricing == nil {
return false return false
} }
return (abs.SelectedPricing).IsPurchased() return (*abs.SelectedPricing).IsPurchased()
} }
func (abs *PricedResource) GetLocationEnd() *time.Time { func (abs *PricedResource) GetLocationEnd() *time.Time {
@ -86,8 +86,8 @@ func (r *PricedResource) GetPrice() (float64, error) {
if len(r.PricingProfiles) == 0 { if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Resource " + r.ResourceID) 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) return pricing.GetPrice(1, 0, *r.UsageStart, *r.UsageEnd)
} }

0
models/resources/processing.go Executable file → Normal file
View File

View File

@ -9,13 +9,13 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "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) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
} }
// New creates a new instance of the bookingMongoAccessor // New creates a new instance of the bookingMongoAccessor
func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor { func NewAccessor(request *tools.APIRequest) *purchaseResourceMongoAccessor {
return &PurchaseResourceMongoAccessor{ return &purchaseResourceMongoAccessor{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.PURCHASE_RESOURCE.String()), // Create a logger with the data type
Request: request, Request: request,
@ -27,23 +27,23 @@ func NewAccessor(request *tools.APIRequest) *PurchaseResourceMongoAccessor {
/* /*
* Nothing special here, just the basic CRUD operations * 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) 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{}) 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) 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) 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) { 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) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(id, a) utils.GenericDeleteOne(id, a)
@ -53,15 +53,15 @@ func (a *PurchaseResourceMongoAccessor) LoadOne(id string) (utils.DBObject, int,
}, a) }, 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) 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) 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 { return func(d utils.DBObject) utils.ShallowDBObject {
if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) { if d.(*PurchaseResource).EndDate != nil && time.Now().UTC().After(*d.(*PurchaseResource).EndDate) {
utils.GenericDeleteOne(d.GetID(), a) utils.GenericDeleteOne(d.GetID(), a)

View File

@ -1,56 +0,0 @@
package purchase_resource_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"cloud.o-forge.io/core/oc-lib/models/resources/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())
})
}

51
models/resources/resource.go Executable file → Normal file
View File

@ -89,10 +89,7 @@ func (r *AbstractInstanciatedResource[T]) GetSelectedInstance() utils.DBObject {
} }
func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest) { func (abs *AbstractInstanciatedResource[T]) SetAllowedInstances(request *tools.APIRequest) {
if request != nil && request.PeerID == abs.CreatorID && request.PeerID != "" { abs.Instances = verifyAuthAction[T](abs.Instances, request)
return
}
abs.Instances = VerifyAuthAction[T](abs.Instances, request)
} }
func (d *AbstractInstanciatedResource[T]) Trim() { func (d *AbstractInstanciatedResource[T]) Trim() {
@ -105,10 +102,10 @@ func (d *AbstractInstanciatedResource[T]) Trim() {
} }
func (abs *AbstractInstanciatedResource[T]) VerifyAuth(request *tools.APIRequest) bool { 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{} instances := []T{}
for _, instance := range baseInstance { for _, instance := range baseInstance {
_, peerGroups := instance.GetPeerGroups() _, peerGroups := instance.GetPeerGroups()
@ -144,24 +141,13 @@ type Credentials struct {
type ResourceInstance[T ResourcePartnerITF] struct { type ResourceInstance[T ResourcePartnerITF] struct {
utils.AbstractObject utils.AbstractObject
Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"` Location GeoPoint `json:"location,omitempty" bson:"location,omitempty"`
Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"` Country countries.CountryCode `json:"country,omitempty" bson:"country,omitempty"`
AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"` AccessProtocol string `json:"access_protocol,omitempty" bson:"access_protocol,omitempty"`
Env []models.Param `json:"env,omitempty" bson:"env,omitempty"` Env []models.Param `json:"env,omitempty" bson:"env,omitempty"`
Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"` Inputs []models.Param `json:"inputs,omitempty" bson:"inputs,omitempty"`
Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"` Outputs []models.Param `json:"outputs,omitempty" bson:"outputs,omitempty"`
SelectedPartnershipIndex *int `json:"selected_partnership_index,omitempty" bson:"selected_partnership_index,omitempty"` // SelectedInstance is the selected instance Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
Partnerships []T `json:"partnerships,omitempty" bson:"partnerships,omitempty"`
}
func (r *ResourceInstance[T]) GetSelectedPartnership() ResourcePartnerITF {
if r.SelectedPartnershipIndex != nil && len(r.Partnerships) > *r.SelectedPartnershipIndex {
return r.Partnerships[*r.SelectedPartnershipIndex]
}
if len(r.Partnerships) > 0 {
return r.Partnerships[0]
}
return nil
} }
func (ri *ResourceInstance[T]) ClearEnv() { func (ri *ResourceInstance[T]) ClearEnv() {
@ -170,20 +156,6 @@ func (ri *ResourceInstance[T]) ClearEnv() {
ri.Outputs = []models.Param{} ri.Outputs = []models.Param{}
} }
func (ri *ResourceInstance[T]) GetPartnerships(peerID string, groups []string) []ResourcePartnerITF {
partners := []ResourcePartnerITF{}
for _, p := range ri.Partnerships {
if p.GetPeerGroups()[peerID] != nil {
for _, g := range p.GetPeerGroups()[peerID] {
if slices.Contains(groups, g) {
partners = append(partners, p)
}
}
}
}
return partners
}
func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF { func (ri *ResourceInstance[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
pricings := []pricing.PricingProfileITF{} pricings := []pricing.PricingProfileITF{}
for _, p := range ri.Partnerships { for _, p := range ri.Partnerships {
@ -217,6 +189,9 @@ type ResourcePartnerShip[T pricing.PricingProfileITF] struct {
func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF { func (ri *ResourcePartnerShip[T]) GetPricingsProfiles(peerID string, groups []string) []pricing.PricingProfileITF {
profiles := []pricing.PricingProfileITF{} profiles := []pricing.PricingProfileITF{}
if ri.PeerGroups[peerID] != nil { if ri.PeerGroups[peerID] != nil {
for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri)
}
if slices.Contains(groups, "*") { if slices.Contains(groups, "*") {
for _, ri := range ri.PricingProfiles { for _, ri := range ri.PricingProfiles {
profiles = append(profiles, ri) profiles = append(profiles, ri)

41
models/resources/resource_accessor.go Executable file → Normal file
View File

@ -1,7 +1,6 @@
package resources package resources
import ( import (
"errors"
"slices" "slices"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
@ -10,17 +9,17 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "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) utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
generateData func() utils.DBObject generateData func() utils.DBObject
} }
// New creates a new instance of the computeMongoAccessor // 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) { if !slices.Contains([]tools.DataType{tools.COMPUTE_RESOURCE, tools.STORAGE_RESOURCE, tools.PROCESSING_RESOURCE, tools.WORKFLOW_RESOURCE, tools.DATA_RESOURCE}, t) {
return nil return nil
} }
return &ResourceMongoAccessor[T]{ return &resourceMongoAccessor[T]{
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(t.String()), // Create a logger with the data type Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
Request: request, Request: request,
@ -33,54 +32,39 @@ func NewAccessor[T ResourceInterface](t tools.DataType, request *tools.APIReques
/* /*
* Nothing special here, just the basic CRUD operations * 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) 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) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't update a non existing computing units resource not reported onto compute units catalog")
}
set.(T).Trim() set.(T).Trim()
return utils.GenericUpdateOne(set, id, dca, dca.generateData()) 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) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't create a non existing computing units resource not reported onto compute units catalog")
}
data.(T).Trim() data.(T).Trim()
return utils.GenericStoreOne(data, dca) 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) {
if dca.GetType() == tools.COMPUTE_RESOURCE {
return nil, 404, errors.New("can't copy/publish a non existing computing units resource not reported onto compute units catalog")
}
return dca.StoreOne(data) 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) { return utils.GenericLoadOne[T](id, func(d utils.DBObject) (utils.DBObject, int, error) {
d.(T).SetAllowedInstances(dca.Request) d.(T).SetAllowedInstances(dca.Request)
return d, 200, nil return d, 200, nil
}, dca) }, 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 { return utils.GenericLoadAll[T](func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request) d.(T).SetAllowedInstances(wfa.Request)
return d return d
}, isDraft, wfa) }, 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)
return d
}, isDraft, wfa)
}
return utils.GenericSearch[T](filters, search, wfa.getResourceFilter(search), return utils.GenericSearch[T](filters, search, wfa.getResourceFilter(search),
func(d utils.DBObject) utils.ShallowDBObject { func(d utils.DBObject) utils.ShallowDBObject {
d.(T).SetAllowedInstances(wfa.Request) d.(T).SetAllowedInstances(wfa.Request)
@ -88,7 +72,10 @@ func (wfa *ResourceMongoAccessor[T]) Search(filters *dbs.Filters, search string,
}, isDraft, wfa) }, isDraft, wfa)
} }
func (abs *ResourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters { func (abs *resourceMongoAccessor[T]) getResourceFilter(search string) *dbs.Filters {
if search == "*" {
search = ""
}
return &dbs.Filters{ return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by like name, short_description, description, owner, url if no filters are provided 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}}, "abstractintanciatedresource.abstractresource.abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},

4
models/resources/storage.go Executable file → Normal file
View File

@ -183,9 +183,9 @@ func (r *PricedStorageResource) GetPrice() (float64, error) {
if len(r.PricingProfiles) == 0 { if len(r.PricingProfiles) == 0 {
return 0, errors.New("pricing profile must be set on Priced Storage" + r.ResourceID) 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 var err error
amountOfData := float64(1) amountOfData := float64(1)
if pricing.GetOverrideStrategyValue() >= 0 { if pricing.GetOverrideStrategyValue() >= 0 {

View File

@ -1,119 +0,0 @@
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)
}

View File

@ -1,125 +0,0 @@
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())
}

View File

@ -1,142 +0,0 @@
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)
})
}

View File

@ -1,115 +0,0 @@
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())
}

View File

@ -1,106 +0,0 @@
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)
}

View File

@ -1,149 +0,0 @@
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())
}

View File

@ -1,62 +0,0 @@
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)
}

0
models/resources/workflow.go Executable file → Normal file
View File

View File

@ -1,38 +0,0 @@
package models
import (
"strconv"
"testing"
"cloud.o-forge.io/core/oc-lib/models"
"github.com/stretchr/testify/assert"
)
func TestModel_ReturnsValidInstances(t *testing.T) {
for name, _ := range models.ModelsCatalog {
t.Run(name, func(t *testing.T) {
modelInt, _ := strconv.Atoi(name)
obj := models.Model(modelInt)
assert.NotNil(t, obj, "Model() returned nil for valid model name %s", name)
})
}
}
func TestModel_UnknownModelReturnsNil(t *testing.T) {
invalidModelInt := -9999 // unlikely to be valid
obj := models.Model(invalidModelInt)
assert.Nil(t, obj)
}
func TestGetModelsNames_ReturnsAllKeys(t *testing.T) {
names := models.GetModelsNames()
assert.Len(t, names, len(models.ModelsCatalog))
seen := make(map[string]bool)
for _, name := range names {
seen[name] = true
}
for key := range models.ModelsCatalog {
assert.Contains(t, seen, key)
}
}

2
models/utils/abstracts.go Executable file → Normal file
View File

@ -91,7 +91,7 @@ func (ao *AbstractObject) UpToDate(user string, peer string, create bool) {
} }
func (ao *AbstractObject) VerifyAuth(request *tools.APIRequest) bool { func (ao *AbstractObject) VerifyAuth(request *tools.APIRequest) bool {
return ao.AccessMode == Public || (request != nil && ao.CreatorID == request.PeerID && request.PeerID != "") return ao.AccessMode == Public || (request != nil && ao.CreatorID == request.PeerID)
} }
func (ao *AbstractObject) GetObjectFilters(search string) *dbs.Filters { func (ao *AbstractObject) GetObjectFilters(search string) *dbs.Filters {

0
models/utils/common.go Executable file → Normal file
View File

0
models/utils/interfaces.go Executable file → Normal file
View File

View File

@ -1,128 +0,0 @@
package models_test
import (
"testing"
"time"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestGenerateID(t *testing.T) {
ao := &utils.AbstractObject{}
ao.GenerateID()
assert.NotEmpty(t, ao.UUID)
_, err := uuid.Parse(ao.UUID)
assert.NoError(t, err)
}
func TestStoreDraftDefault(t *testing.T) {
ao := &utils.AbstractObject{IsDraft: true}
ao.StoreDraftDefault()
assert.False(t, ao.IsDraft)
}
func TestCanUpdate(t *testing.T) {
ao := &utils.AbstractObject{}
res, set := ao.CanUpdate(nil)
assert.True(t, res)
assert.Nil(t, set)
}
func TestCanDelete(t *testing.T) {
ao := &utils.AbstractObject{}
assert.True(t, ao.CanDelete())
}
func TestIsDrafted(t *testing.T) {
ao := &utils.AbstractObject{IsDraft: true}
assert.True(t, ao.IsDrafted())
}
func TestGetID(t *testing.T) {
u := uuid.New().String()
ao := &utils.AbstractObject{UUID: u}
assert.Equal(t, u, ao.GetID())
}
func TestGetName(t *testing.T) {
name := "MyObject"
ao := &utils.AbstractObject{Name: name}
assert.Equal(t, name, ao.GetName())
}
func TestGetCreatorID(t *testing.T) {
id := "creator-123"
ao := &utils.AbstractObject{CreatorID: id}
assert.Equal(t, id, ao.GetCreatorID())
}
func TestUpToDate_CreateFalse(t *testing.T) {
ao := &utils.AbstractObject{}
now := time.Now()
time.Sleep(time.Millisecond) // ensure time difference
ao.UpToDate("user123", "peer456", false)
assert.WithinDuration(t, now, ao.UpdateDate, time.Second)
assert.Equal(t, "peer456", ao.UpdaterID)
assert.Equal(t, "user123", ao.UserUpdaterID)
assert.True(t, ao.CreationDate.IsZero())
}
func TestUpToDate_CreateTrue(t *testing.T) {
ao := &utils.AbstractObject{}
now := time.Now()
time.Sleep(time.Millisecond)
ao.UpToDate("user123", "peer456", true)
assert.WithinDuration(t, now, ao.UpdateDate, time.Second)
assert.WithinDuration(t, now, ao.CreationDate, time.Second)
assert.Equal(t, "peer456", ao.UpdaterID)
assert.Equal(t, "peer456", ao.CreatorID)
assert.Equal(t, "user123", ao.UserUpdaterID)
assert.Equal(t, "user123", ao.UserCreatorID)
}
func TestVerifyAuth(t *testing.T) {
request := &tools.APIRequest{PeerID: "peer123"}
ao := &utils.AbstractObject{CreatorID: "peer123"}
assert.True(t, ao.VerifyAuth(request))
ao = &utils.AbstractObject{AccessMode: utils.Public}
assert.True(t, ao.VerifyAuth(nil))
ao = &utils.AbstractObject{AccessMode: utils.Private, CreatorID: "peer123"}
request = &tools.APIRequest{PeerID: "wrong"}
assert.False(t, ao.VerifyAuth(request))
}
func TestGetObjectFilters(t *testing.T) {
ao := &utils.AbstractObject{}
f := ao.GetObjectFilters("*")
assert.NotNil(t, f)
assert.Contains(t, f.Or, "abstractobject.name")
assert.Equal(t, dbs.LIKE.String(), f.Or["abstractobject.name"][0].Operator)
}
func TestDeserialize(t *testing.T) {
ao := &utils.AbstractObject{}
input := map[string]interface{}{"name": "test", "id": uuid.New().String()}
res := ao.Deserialize(input, &utils.AbstractObject{})
assert.NotNil(t, res)
}
func TestSerialize(t *testing.T) {
ao := &utils.AbstractObject{Name: "test", UUID: uuid.New().String()}
m := ao.Serialize(ao)
assert.Equal(t, "test", m["name"])
}
func TestAbstractAccessorMethods(t *testing.T) {
r := &utils.AbstractAccessor{Request: &tools.APIRequest{Username: "alice", PeerID: "peer1", Groups: []string{"dev"}}}
assert.True(t, r.ShouldVerifyAuth())
assert.Equal(t, "alice", r.GetUser())
assert.Equal(t, "peer1", r.GetPeerID())
assert.Equal(t, []string{"dev"}, r.GetGroups())
assert.Equal(t, r.Request.Caller, r.GetCaller())
}

View File

@ -1,168 +0,0 @@
package models_test
import (
"errors"
"testing"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// --- Mock Definitions ---
type MockDBObject struct {
mock.Mock
}
func (m *MockAccessor) GetLogger() *zerolog.Logger {
return nil
}
func (m *MockAccessor) GetGroups() []string {
return []string{}
}
func (m *MockAccessor) GetCaller() *tools.HTTPCaller {
return nil
}
func (m *MockDBObject) GenerateID() { m.Called() }
func (m *MockDBObject) StoreDraftDefault() { m.Called() }
func (m *MockDBObject) UpToDate(user, peer string, create bool) {
m.Called(user, peer, create)
}
func (m *MockDBObject) VerifyAuth(req *tools.APIRequest) bool {
args := m.Called(req)
return args.Bool(0)
}
func (m *MockDBObject) CanDelete() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockDBObject) CanUpdate(set utils.DBObject) (bool, utils.DBObject) {
args := m.Called(set)
return args.Bool(0), args.Get(1).(utils.DBObject)
}
func (m *MockDBObject) IsDrafted() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockDBObject) Serialize(obj utils.DBObject) map[string]interface{} {
args := m.Called(obj)
return args.Get(0).(map[string]interface{})
}
func (m *MockDBObject) Deserialize(mdata map[string]interface{}, obj utils.DBObject) utils.DBObject {
args := m.Called(mdata, obj)
return args.Get(0).(utils.DBObject)
}
func (m *MockDBObject) GetID() string {
args := m.Called()
return args.String(0)
}
func (m *MockDBObject) GetName() string {
args := m.Called()
return args.String(0)
}
type MockAccessor struct {
mock.Mock
}
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) ShouldVerifyAuth() bool {
args := m.Called()
return args.Bool(0)
}
func (m *MockAccessor) GetRequest() *tools.APIRequest {
args := m.Called()
return args.Get(0).(*tools.APIRequest)
}
func (m *MockAccessor) GetType() tools.DataType {
args := m.Called()
return args.Get(0).(tools.DataType)
}
func (m *MockAccessor) Search(filters *dbs.Filters, s string, d bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, s, d)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockAccessor) GetUser() string {
args := m.Called()
return args.String(0)
}
func (m *MockAccessor) GetPeerID() string {
args := m.Called()
return args.String(0)
}
// --- Test Cases ---
func TestVerifyAccess_Authorized(t *testing.T) {
mockObj := new(MockDBObject)
mockAcc := new(MockAccessor)
req := &tools.APIRequest{PeerID: "peer"}
mockAcc.On("LoadOne", "123").Return(mockObj, 200, nil)
mockAcc.On("ShouldVerifyAuth").Return(true)
mockObj.On("VerifyAuth", req).Return(true)
mockAcc.On("GetRequest").Return(req)
err := utils.VerifyAccess(mockAcc, "123")
assert.NoError(t, err)
}
func TestVerifyAccess_Unauthorized(t *testing.T) {
mockObj := new(MockDBObject)
mockAcc := new(MockAccessor)
req := &tools.APIRequest{PeerID: "peer"}
mockAcc.On("LoadOne", "123").Return(mockObj, 200, nil)
mockAcc.On("ShouldVerifyAuth").Return(true)
mockObj.On("VerifyAuth", req).Return(false)
mockAcc.On("GetRequest").Return(req)
err := utils.VerifyAccess(mockAcc, "123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not allowed")
}
func TestVerifyAccess_LoadError(t *testing.T) {
mockAcc := new(MockAccessor)
mockAcc.On("LoadOne", "bad-id").Return(nil, 404, errors.New("not found"))
err := utils.VerifyAccess(mockAcc, "bad-id")
assert.Error(t, err)
assert.Equal(t, "not found", err.Error())
}

View File

@ -31,43 +31,6 @@ func (d *Workflow) GetAccessor(request *tools.APIRequest) utils.Accessor {
return NewAccessor(request) // Create a new instance of the accessor return NewAccessor(request) // Create a new instance of the accessor
} }
type Deps struct {
Source string
Dest string
}
func (w *Workflow) IsDependancy(id string) []Deps {
dependancyOfIDs := []Deps{}
for _, link := range w.Graph.Links {
if _, ok := w.Graph.Items[link.Destination.ID]; !ok {
continue
}
source := w.Graph.Items[link.Destination.ID].Processing
if id == link.Source.ID && source != nil {
dependancyOfIDs = append(dependancyOfIDs, Deps{Source: source.GetName(), Dest: link.Destination.ID})
}
sourceWF := w.Graph.Items[link.Destination.ID].Workflow
if id == link.Source.ID && sourceWF != nil {
dependancyOfIDs = append(dependancyOfIDs, Deps{Source: sourceWF.GetName(), Dest: link.Destination.ID})
}
}
return dependancyOfIDs
}
func (w *Workflow) GetDependencies(id string) (dependencies []Deps) {
for _, link := range w.Graph.Links {
if _, ok := w.Graph.Items[link.Source.ID]; !ok {
continue
}
source := w.Graph.Items[link.Source.ID].Processing
if id == link.Destination.ID && source != nil {
dependencies = append(dependencies, Deps{Source: source.GetName(), Dest: link.Source.ID})
continue
}
}
return
}
func (w *Workflow) GetGraphItems(f func(item graph.GraphItem) bool) (list_datas []graph.GraphItem) { func (w *Workflow) GetGraphItems(f func(item graph.GraphItem) bool) (list_datas []graph.GraphItem) {
for _, item := range w.Graph.Items { for _, item := range w.Graph.Items {
if f(item) { if f(item) {

View File

@ -95,7 +95,7 @@ func (a *workflowMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.
if set.(*Workflow).Graph != nil && set.(*Workflow).Graph.Partial { if set.(*Workflow).Graph != nil && set.(*Workflow).Graph.Partial {
return nil, 403, errors.New("you are not allowed to update a partial workflow") return nil, 403, errors.New("you are not allowed to update a partial workflow")
} }
res, code, err := utils.GenericUpdateOne(set, id, a, &Workflow{}) res, code, err := utils.GenericUpdateOne(a.verifyResource(set), id, a, &Workflow{})
if code != 200 { if code != 200 {
return nil, code, err return nil, code, err
} }

View File

@ -1,149 +0,0 @@
package workflow_execution_test
import (
"testing"
"time"
"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/models/workflow"
"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockAccessor struct {
mock.Mock
}
func (m *MockAccessor) LoadOne(id string) (interface{}, int, error) {
args := m.Called(id)
return args.Get(0), args.Int(1), args.Error(2)
}
func TestNewScheduler_ValidInput(t *testing.T) {
s := "2025-06-16T15:00:00"
e := "2025-06-16T17:00:00"
dur := 7200.0
cronStr := "0 0 * * * *"
sched := workflow_execution.NewScheduler(s, e, dur, cronStr)
assert.NotNil(t, sched)
assert.Equal(t, dur, sched.DurationS)
assert.Equal(t, cronStr, sched.Cron)
}
func TestNewScheduler_InvalidStart(t *testing.T) {
s := "invalid"
e := "2025-06-16T17:00:00"
dur := 7200.0
cronStr := "0 0 * * * *"
sched := workflow_execution.NewScheduler(s, e, dur, cronStr)
assert.Nil(t, sched)
}
func TestNewScheduler_InvalidEnd(t *testing.T) {
s := "2025-06-16T15:00:00"
e := "invalid"
dur := 7200.0
cronStr := "0 0 * * * *"
sched := workflow_execution.NewScheduler(s, e, dur, cronStr)
assert.NotNil(t, sched)
assert.Nil(t, sched.End)
}
func TestGetDates_NoCron(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
s := &workflow_execution.WorkflowSchedule{
Start: start,
End: &end,
}
schedule, err := s.GetDates()
assert.NoError(t, err)
assert.Len(t, schedule, 1)
assert.Equal(t, start, schedule[0].Start)
assert.Equal(t, end, *schedule[0].End)
}
func TestGetDates_InvalidCron(t *testing.T) {
start := time.Now()
end := start.Add(2 * time.Hour)
s := &workflow_execution.WorkflowSchedule{
Start: start,
End: &end,
Cron: "bad cron",
}
_, err := s.GetDates()
assert.Error(t, err)
}
func TestGetDates_ValidCron(t *testing.T) {
start := time.Now()
end := start.Add(10 * time.Minute)
s := &workflow_execution.WorkflowSchedule{
Start: start,
End: &end,
DurationS: 60,
Cron: "0 */2 * * * *",
}
dates, err := s.GetDates()
assert.NoError(t, err)
assert.Greater(t, len(dates), 0)
}
func TestGetExecutions_Success(t *testing.T) {
start := time.Now()
end := start.Add(1 * time.Hour)
ws := &workflow_execution.WorkflowSchedule{
UUID: uuid.New().String(),
Start: start,
End: &end,
}
wf := &workflow.Workflow{
AbstractObject: utils.AbstractObject{
UUID: uuid.New().String(),
Name: "TestWorkflow",
},
}
execs, err := ws.GetExecutions(wf)
assert.NoError(t, err)
assert.Greater(t, len(execs), 0)
assert.Equal(t, wf.UUID, execs[0].WorkflowID)
assert.Equal(t, ws.UUID, execs[0].ExecutionsID)
assert.Equal(t, enum.DRAFT, execs[0].State)
}
func TestSchedules_NoRequest(t *testing.T) {
ws := &workflow_execution.WorkflowSchedule{}
ws, wf, execs, err := ws.Schedules("someID", nil)
assert.Error(t, err)
assert.Nil(t, wf)
assert.Len(t, execs, 0)
assert.Equal(t, ws, ws)
}
// Additional test stubs to be completed with gomock usage for:
// - CheckBooking
// - BookExecs
// - getBooking
// - Schedules (success path)
// - Planify mocking in CheckBooking
// - Peer interaction in BookExecs
// - Caller deep copy errors in getCallerCopy
// Will be continued...

View File

@ -1,154 +0,0 @@
package workflow_execution_test
import (
"testing"
"time"
"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/models/workflow_execution"
"cloud.o-forge.io/core/oc-lib/tools"
"github.com/stretchr/testify/assert"
)
func (m *MockAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return nil, args.Int(1), args.Error(2)
}
func (m *MockAccessor) Search(filters interface{}, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func TestStoreDraftDefault(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.StoreDraftDefault()
assert.False(t, exec.IsDraft)
assert.Equal(t, enum.SCHEDULED, exec.State)
}
func TestCanUpdate_StateChange(t *testing.T) {
existing := &workflow_execution.WorkflowExecution{State: enum.DRAFT}
newExec := &workflow_execution.WorkflowExecution{State: enum.SCHEDULED}
canUpdate, updated := existing.CanUpdate(newExec)
assert.True(t, canUpdate)
assert.Equal(t, enum.SCHEDULED, updated.(*workflow_execution.WorkflowExecution).State)
}
func TestCanUpdate_SameState_Draft(t *testing.T) {
existing := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}, State: enum.DRAFT}
newExec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}, State: enum.DRAFT}
canUpdate, _ := existing.CanUpdate(newExec)
assert.False(t, canUpdate)
}
func TestCanDelete_TrueIfDraft(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: true}}
assert.True(t, exec.CanDelete())
}
func TestCanDelete_FalseIfNotDraft(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{IsDraft: false}}
assert.False(t, exec.CanDelete())
}
func TestEquals_True(t *testing.T) {
d := time.Now()
exec1 := &workflow_execution.WorkflowExecution{ExecDate: d, WorkflowID: "123"}
exec2 := &workflow_execution.WorkflowExecution{ExecDate: d, WorkflowID: "123"}
assert.True(t, exec1.Equals(exec2))
}
func TestEquals_False(t *testing.T) {
exec1 := &workflow_execution.WorkflowExecution{ExecDate: time.Now(), WorkflowID: "abc"}
exec2 := &workflow_execution.WorkflowExecution{ExecDate: time.Now().Add(time.Hour), WorkflowID: "def"}
assert.False(t, exec1.Equals(exec2))
}
func TestArgoStatusToState_Success(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.ArgoStatusToState("succeeded")
assert.Equal(t, enum.SUCCESS, exec.State)
}
func TestArgoStatusToState_DefaultToFailure(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.ArgoStatusToState("unknown")
assert.Equal(t, enum.FAILURE, exec.State)
}
func TestGenerateID_AssignsUUID(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
exec.GenerateID()
assert.NotEmpty(t, exec.UUID)
}
func TestGetName_ReturnsCorrectFormat(t *testing.T) {
time := time.Now()
exec := &workflow_execution.WorkflowExecution{AbstractObject: utils.AbstractObject{UUID: "abc"}, ExecDate: time}
assert.Contains(t, exec.GetName(), "abc")
assert.Contains(t, exec.GetName(), time.String())
}
func TestVerifyAuth_AlwaysTrue(t *testing.T) {
exec := &workflow_execution.WorkflowExecution{}
assert.True(t, exec.VerifyAuth(nil))
}
func TestUpdateOne_RejectsZeroState(t *testing.T) {
accessor := &workflow_execution.WorkflowExecutionMongoAccessor{}
set := &workflow_execution.WorkflowExecution{State: 0}
_, code, err := accessor.UpdateOne(set, "someID")
assert.Equal(t, 400, code)
assert.Error(t, err)
}
func TestLoadOne_DraftExpired_ShouldDelete(t *testing.T) {
// Normally would mock time.Now and delete call; for now we test structure
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
exec := &workflow_execution.WorkflowExecution{
ExecDate: time.Now().Add(-2 * time.Minute),
State: enum.DRAFT,
AbstractObject: utils.AbstractObject{UUID: "to-delete"},
}
_, _, _ = accessor.LoadOne(exec.GetID())
// No panic = good enough placeholder
}
func TestLoadOne_ScheduledExpired_ShouldUpdateToForgotten(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
exec := &workflow_execution.WorkflowExecution{
ExecDate: time.Now().Add(-2 * time.Minute),
State: enum.SCHEDULED,
AbstractObject: utils.AbstractObject{UUID: "to-forget"},
}
_, _, _ = accessor.LoadOne(exec.GetID())
}
func TestDeleteOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.DeleteOne("someID")
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestStoreOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.StoreOne(nil)
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestCopyOne_NotImplemented(t *testing.T) {
accessor := workflow_execution.NewAccessor(&tools.APIRequest{})
_, code, err := accessor.CopyOne(nil)
assert.Equal(t, 404, code)
assert.Error(t, err)
}
func TestGetExecFilters_BasicPattern(t *testing.T) {
a := workflow_execution.NewAccessor(&tools.APIRequest{})
filters := a.GetExecFilters("foo")
assert.Contains(t, filters.Or["abstractobject.name"][0].Value, "foo")
}

2
models/workflow_execution/workflow_execution.go Executable file → Normal file
View File

@ -112,8 +112,6 @@ func (d *WorkflowExecution) VerifyAuth(request *tools.APIRequest) bool {
func (d *WorkflowExecution) Book(executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*booking.Booking { func (d *WorkflowExecution) Book(executionsID string, wfID string, priceds map[tools.DataType]map[string]pricing.PricedItemITF) []*booking.Booking {
booking := d.bookEach(executionsID, wfID, tools.STORAGE_RESOURCE, priceds[tools.STORAGE_RESOURCE]) booking := d.bookEach(executionsID, wfID, tools.STORAGE_RESOURCE, priceds[tools.STORAGE_RESOURCE])
booking = append(booking, d.bookEach(executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])...) booking = append(booking, d.bookEach(executionsID, wfID, tools.PROCESSING_RESOURCE, priceds[tools.PROCESSING_RESOURCE])...)
booking = append(booking,d.bookEach(executionsID, wfID, tools.COMPUTE_RESOURCE, priceds[tools.COMPUTE_RESOURCE])...)
booking = append(booking,d.bookEach(executionsID, wfID, tools.DATA_RESOURCE, priceds[tools.DATA_RESOURCE])...)
return booking return booking
} }

View File

@ -11,13 +11,13 @@ import (
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type WorkflowExecutionMongoAccessor struct { type workflowExecutionMongoAccessor struct {
utils.AbstractAccessor utils.AbstractAccessor
shallow bool shallow bool
} }
func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor { func newShallowAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{ return &workflowExecutionMongoAccessor{
shallow: true, shallow: true,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
@ -27,8 +27,8 @@ func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccess
} }
} }
func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor { func NewAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor {
return &WorkflowExecutionMongoAccessor{ return &workflowExecutionMongoAccessor{
shallow: false, shallow: false,
AbstractAccessor: utils.AbstractAccessor{ AbstractAccessor: utils.AbstractAccessor{
Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type Logger: logs.CreateLogger(tools.WORKFLOW_EXECUTION.String()), // Create a logger with the data type
@ -38,11 +38,11 @@ func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor {
} }
} }
func (wfa *WorkflowExecutionMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) { func (wfa *workflowExecutionMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented") return nil, 404, errors.New("not implemented")
} }
func (wfa *WorkflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) { func (wfa *workflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
if set.(*WorkflowExecution).State == 0 { if set.(*WorkflowExecution).State == 0 {
return nil, 400, errors.New("state is required") return nil, 400, errors.New("state is required")
} }
@ -50,23 +50,21 @@ func (wfa *WorkflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id stri
return utils.GenericUpdateOne(&realSet, id, wfa, &WorkflowExecution{}) return utils.GenericUpdateOne(&realSet, id, wfa, &WorkflowExecution{})
} }
func (wfa *WorkflowExecutionMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func (wfa *workflowExecutionMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented") return nil, 404, errors.New("not implemented")
} }
func (wfa *WorkflowExecutionMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) { func (wfa *workflowExecutionMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
return nil, 404, errors.New("not implemented") return nil, 404, errors.New("not implemented")
} }
func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) { func (a *workflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
return utils.GenericLoadOne[*WorkflowExecution](id, func(d utils.DBObject) (utils.DBObject, int, error) { return utils.GenericLoadOne[*WorkflowExecution](id, func(d utils.DBObject) (utils.DBObject, int, error) {
now := time.Now() if d.(*WorkflowExecution).State == enum.DRAFT && !a.shallow && time.Now().UTC().After(d.(*WorkflowExecution).ExecDate) {
now = now.Add(time.Second * -60)
if d.(*WorkflowExecution).State == enum.DRAFT && !a.shallow && now.UTC().After(d.(*WorkflowExecution).ExecDate) {
utils.GenericDeleteOne(d.GetID(), newShallowAccessor(a.Request)) utils.GenericDeleteOne(d.GetID(), newShallowAccessor(a.Request))
return nil, 404, errors.New("not found") return nil, 404, errors.New("not found")
} }
if d.(*WorkflowExecution).State == enum.SCHEDULED && !a.shallow && now.UTC().After(d.(*WorkflowExecution).ExecDate) { if d.(*WorkflowExecution).State == enum.SCHEDULED && !a.shallow && time.Now().UTC().After(d.(*WorkflowExecution).ExecDate) {
d.(*WorkflowExecution).State = enum.FORGOTTEN d.(*WorkflowExecution).State = enum.FORGOTTEN
utils.GenericRawUpdateOne(d, id, newShallowAccessor(a.Request)) utils.GenericRawUpdateOne(d, id, newShallowAccessor(a.Request))
} }
@ -74,23 +72,21 @@ func (a *WorkflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int
}, a) }, a)
} }
func (a *WorkflowExecutionMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) { func (a *workflowExecutionMongoAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericLoadAll[*WorkflowExecution](a.getExec(), isDraft, a) return utils.GenericLoadAll[*WorkflowExecution](a.getExec(), isDraft, a)
} }
func (a *WorkflowExecutionMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) { func (a *workflowExecutionMongoAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
return utils.GenericSearch[*WorkflowExecution](filters, search, a.GetExecFilters(search), a.getExec(), isDraft, a) return utils.GenericSearch[*WorkflowExecution](filters, search, a.GetExecFilters(search), a.getExec(), isDraft, a)
} }
func (a *WorkflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject { func (a *workflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.ShallowDBObject {
return func(d utils.DBObject) utils.ShallowDBObject { return func(d utils.DBObject) utils.ShallowDBObject {
now := time.Now() if d.(*WorkflowExecution).State == enum.DRAFT && time.Now().UTC().After(d.(*WorkflowExecution).ExecDate) {
now = now.Add(time.Second * -60)
if d.(*WorkflowExecution).State == enum.DRAFT && now.UTC().After(d.(*WorkflowExecution).ExecDate) {
utils.GenericDeleteOne(d.GetID(), newShallowAccessor(a.Request)) utils.GenericDeleteOne(d.GetID(), newShallowAccessor(a.Request))
return nil return nil
} }
if d.(*WorkflowExecution).State == enum.SCHEDULED && now.UTC().After(d.(*WorkflowExecution).ExecDate) { if d.(*WorkflowExecution).State == enum.SCHEDULED && time.Now().UTC().After(d.(*WorkflowExecution).ExecDate) {
d.(*WorkflowExecution).State = enum.FORGOTTEN d.(*WorkflowExecution).State = enum.FORGOTTEN
utils.GenericRawUpdateOne(d, d.GetID(), newShallowAccessor(a.Request)) utils.GenericRawUpdateOne(d, d.GetID(), newShallowAccessor(a.Request))
return d return d
@ -99,7 +95,7 @@ func (a *WorkflowExecutionMongoAccessor) getExec() func(utils.DBObject) utils.Sh
} }
} }
func (a *WorkflowExecutionMongoAccessor) GetExecFilters(search string) *dbs.Filters { func (a *workflowExecutionMongoAccessor) GetExecFilters(search string) *dbs.Filters {
return &dbs.Filters{ return &dbs.Filters{
Or: map[string][]dbs.Filter{ // filter by name if no filters are provided Or: map[string][]dbs.Filter{ // filter by name if no filters are provided
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}}, "abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}},

108
models/workflow_execution/workflow_scheduler.go Executable file → Normal file
View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"time" "time"
"cloud.o-forge.io/core/oc-lib/models/booking" "cloud.o-forge.io/core/oc-lib/models/booking"
@ -71,67 +70,29 @@ func (ws *WorkflowSchedule) CheckBooking(wfID string, request *tools.APIRequest)
if ws.End != nil && ws.Start.Add(time.Duration(longest)*time.Second).After(*ws.End) { if ws.End != nil && ws.Start.Add(time.Duration(longest)*time.Second).After(*ws.End) {
ws.Warning = "The workflow may be too long to be executed in the given time frame, we will try to book it anyway\n" ws.Warning = "The workflow may be too long to be executed in the given time frame, we will try to book it anyway\n"
} }
execs, err := ws.GetExecutions(wf) execs, err := ws.getExecutions(wf)
if err != nil { if err != nil {
return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err
} }
bookings := []*booking.Booking{} bookings := []*booking.Booking{}
for _, exec := range execs { for _, exec := range execs {
bookings = append(bookings, exec.Book(ws.UUID, wfID, priceds)...) bookings = append(bookings, exec.Book(ws.UUID, wfID, priceds)...)
} for _, b := range bookings {
meth := request.Caller.URLS[tools.BOOKING][tools.GET]
errCh := make(chan error, len(bookings)) meth = strings.ReplaceAll(meth, ":id", b.ResourceID)
var m sync.Mutex meth = strings.ReplaceAll(meth, ":start_date", b.ExpectedStartDate.Format("2006-01-02T15:04:05"))
meth = strings.ReplaceAll(meth, ":end_date", b.ExpectedEndDate.Format("2006-01-02T15:04:05"))
for _, b := range bookings { request.Caller.URLS[tools.BOOKING][tools.GET] = meth
go getBooking(b, request, wf, execs, bookings, errCh, &m) _, err := (&peer.Peer{}).LaunchPeerExecution(b.DestPeerID, b.ResourceID, tools.BOOKING, tools.GET, nil, request.Caller)
} if err != nil {
return false, wf, execs, bookings, err
for i := 0; i < len(bookings); i++ { }
if err := <-errCh; err != nil {
return false, wf, execs, bookings, err
} }
}
}
return true, wf, execs, bookings, nil return true, wf, execs, bookings, nil
} }
func getBooking(b *booking.Booking, request *tools.APIRequest, wf *workflow.Workflow, execs []*WorkflowExecution, bookings []*booking.Booking, errCh chan error, m *sync.Mutex) {
m.Lock()
c, err := getCallerCopy(request, errCh)
if err != nil {
errCh <- err
return
}
m.Unlock()
meth := c.URLS[tools.BOOKING][tools.GET]
meth = strings.ReplaceAll(meth, ":id", b.ResourceID)
meth = strings.ReplaceAll(meth, ":start_date", b.ExpectedStartDate.Format("2006-01-02T15:04:05"))
meth = strings.ReplaceAll(meth, ":end_date", b.ExpectedEndDate.Format("2006-01-02T15:04:05"))
c.URLS[tools.BOOKING][tools.GET] = meth
_, err = (&peer.Peer{}).LaunchPeerExecution(b.DestPeerID, b.ResourceID, tools.BOOKING, tools.GET, nil, &c)
if err != nil {
errCh <- err
return
}
errCh <- nil
}
func getCallerCopy(request *tools.APIRequest, errCh chan error) (tools.HTTPCaller, error) {
var c tools.HTTPCaller
err := request.Caller.DeepCopy(c)
if err != nil {
errCh <- err
return tools.HTTPCaller{}, nil
}
c.URLS = request.Caller.URLS
return c, err
}
func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*WorkflowSchedule, *workflow.Workflow, []*WorkflowExecution, error) { func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*WorkflowSchedule, *workflow.Workflow, []*WorkflowExecution, error) {
if request == nil { if request == nil {
return ws, nil, []*WorkflowExecution{}, errors.New("no request found") return ws, nil, []*WorkflowExecution{}, errors.New("no request found")
@ -150,54 +111,25 @@ func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*
return ws, nil, executions, errors.New("could not book the workflow : " + fmt.Sprintf("%v", err)) return ws, nil, executions, errors.New("could not book the workflow : " + fmt.Sprintf("%v", err))
} }
ws.Workflow = wf ws.Workflow = wf
var errCh = make(chan error, len(bookings))
var m sync.Mutex
for _, booking := range bookings { for _, booking := range bookings {
go ws.BookExecs(booking, request, errCh, &m) _, err := (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "",
} tools.BOOKING, tools.POST, booking.Serialize(booking), request.Caller)
if err != nil {
for i := 0; i < len(bookings); i++ {
if err := <-errCh; err != nil {
return ws, wf, executions, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err)) return ws, wf, executions, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err))
} }
} }
fmt.Println("Schedules")
for _, exec := range executions { for _, exec := range executions {
err := exec.PurgeDraft(request) err := exec.PurgeDraft(request)
if err != nil { if err != nil {
return ws, nil, []*WorkflowExecution{}, errors.New("purge draft" + fmt.Sprintf("%v", err)) return ws, nil, []*WorkflowExecution{}, errors.New("purge draft" + fmt.Sprintf("%v", err))
} }
exec.StoreDraftDefault() exec.StoreDraftDefault()
utils.GenericStoreOne(exec, NewAccessor(request)) // Should DELETE the previous execution2
fmt.Println(utils.GenericStoreOne(exec, NewAccessor(request)))
} }
fmt.Println("Schedules")
return ws, wf, executions, nil return ws, wf, executions, nil
} }
func (ws *WorkflowSchedule) BookExecs(booking *booking.Booking, request *tools.APIRequest, errCh chan error, m *sync.Mutex) {
m.Lock()
c, err := getCallerCopy(request, errCh)
if err != nil {
errCh <- err
return
}
m.Unlock()
_, err = (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "",
tools.BOOKING, tools.POST, booking.Serialize(booking), &c)
if err != nil {
errCh <- err
return
}
errCh <- nil
}
/* /*
BOOKING IMPLIED TIME, not of subscription but of execution BOOKING IMPLIED TIME, not of subscription but of execution
so is processing time execution time applied on computes so is processing time execution time applied on computes
@ -210,9 +142,9 @@ VERIFY THAT WE HANDLE DIFFERENCE BETWEEN LOCATION TIME && BOOKING
* getExecutions is a function that returns the executions of a workflow * getExecutions is a function that returns the executions of a workflow
* it returns an array of workflow_execution.WorkflowExecution * it returns an array of workflow_execution.WorkflowExecution
*/ */
func (ws *WorkflowSchedule) GetExecutions(workflow *workflow.Workflow) ([]*WorkflowExecution, error) { func (ws *WorkflowSchedule) getExecutions(workflow *workflow.Workflow) ([]*WorkflowExecution, error) {
workflows_executions := []*WorkflowExecution{} workflows_executions := []*WorkflowExecution{}
dates, err := ws.GetDates() dates, err := ws.getDates()
if err != nil { if err != nil {
return workflows_executions, err return workflows_executions, err
} }
@ -233,7 +165,7 @@ func (ws *WorkflowSchedule) GetExecutions(workflow *workflow.Workflow) ([]*Workf
return workflows_executions, nil return workflows_executions, nil
} }
func (ws *WorkflowSchedule) GetDates() ([]Schedule, error) { func (ws *WorkflowSchedule) getDates() ([]Schedule, error) {
schedule := []Schedule{} schedule := []Schedule{}
if len(ws.Cron) > 0 { // if cron is set then end date should be set if len(ws.Cron) > 0 { // if cron is set then end date should be set
if ws.End == nil { if ws.End == nil {

View File

@ -1,215 +0,0 @@
// File: workspace_accessor_test.go
package workspace_test
import (
"errors"
"testing"
"cloud.o-forge.io/core/oc-lib/dbs"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/models/workspace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockWorkspaceAccessor struct {
mock.Mock
workspace.Workspace
}
func (m *MockWorkspaceAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
args := m.Called(data)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
args := m.Called(set, id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) LoadOne(id string) (utils.DBObject, int, error) {
args := m.Called(id)
return args.Get(0).(utils.DBObject), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) LoadAll(isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func (m *MockWorkspaceAccessor) Search(filters *dbs.Filters, search string, isDraft bool) ([]utils.ShallowDBObject, int, error) {
args := m.Called(filters, search, isDraft)
return args.Get(0).([]utils.ShallowDBObject), args.Int(1), args.Error(2)
}
func TestStoreOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{Name: "test_ws"}}
mockAcc.On("StoreOne", ws).Return(ws, 200, nil)
res, code, err := mockAcc.StoreOne(ws)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
mockAcc.AssertExpectations(t)
}
func TestStoreOne_Conflict(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{Name: "duplicate"}}
mockAcc.On("StoreOne", ws).Return(nil, 409, errors.New("a workspace with the same name already exists"))
res, code, err := mockAcc.StoreOne(ws)
assert.Error(t, err)
assert.Equal(t, 409, code)
assert.Nil(t, res)
}
func TestUpdateOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "123", IsDraft: false}}
mockAcc.On("UpdateOne", ws, "123").Return(ws, 200, nil)
res, code, err := mockAcc.UpdateOne(ws, "123")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
}
func TestUpdateOne_Error(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "999"}}
err := errors.New("update failed")
mockAcc.On("UpdateOne", ws, "999").Return(nil, 500, err)
res, code, err := mockAcc.UpdateOne(ws, "999")
assert.Error(t, err)
assert.Equal(t, 500, code)
assert.Nil(t, res)
}
func TestDeleteOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "321"}}
mockAcc.On("DeleteOne", "321").Return(ws, 200, nil)
res, code, err := mockAcc.DeleteOne("321")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
}
func TestDeleteOne_NotFound(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
err := errors.New("not found")
mockAcc.On("DeleteOne", "notfound").Return(nil, 404, err)
res, code, err := mockAcc.DeleteOne("notfound")
assert.Error(t, err)
assert.Equal(t, 404, code)
assert.Nil(t, res)
}
func TestLoadOne_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "loadid"}}
mockAcc.On("LoadOne", "loadid").Return(ws, 200, nil)
res, code, err := mockAcc.LoadOne("loadid")
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, ws, res)
}
func TestLoadOne_Error(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
err := errors.New("db error")
mockAcc.On("LoadOne", "badid").Return(nil, 500, err)
res, code, err := mockAcc.LoadOne("badid")
assert.Error(t, err)
assert.Equal(t, 500, code)
assert.Nil(t, res)
}
func TestLoadAll_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
ws := &workspace.Workspace{AbstractObject: utils.AbstractObject{UUID: "all1"}}
mockAcc.On("LoadAll", true).Return([]utils.ShallowDBObject{ws}, 200, nil)
res, code, err := mockAcc.LoadAll(true)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Len(t, res, 1)
}
func TestLoadAll_Empty(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("LoadAll", false).Return([]utils.ShallowDBObject{}, 200, nil)
res, code, err := mockAcc.LoadAll(false)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.Empty(t, res)
}
func TestSearch_Success(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
filters := &dbs.Filters{}
mockAcc.On("Search", filters, "keyword", true).Return([]utils.ShallowDBObject{}, 200, nil)
res, code, err := mockAcc.Search(filters, "keyword", true)
assert.NoError(t, err)
assert.Equal(t, 200, code)
assert.NotNil(t, res)
}
func TestSearch_Error(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
filters := &dbs.Filters{}
err := errors.New("search failed")
mockAcc.On("Search", filters, "fail", false).Return(nil, 500, err)
res, code, err := mockAcc.Search(filters, "fail", false)
assert.Error(t, err)
assert.Equal(t, 500, code)
assert.Nil(t, res)
}
// Additional edge test cases
func TestStoreOne_InvalidType(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("StoreOne", mock.Anything).Return(nil, 400, errors.New("invalid type"))
res, code, err := mockAcc.StoreOne(&utils.AbstractObject{})
assert.Error(t, err)
assert.Equal(t, 400, code)
assert.Nil(t, res)
}
func TestUpdateOne_NilData(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("UpdateOne", nil, "id").Return(nil, 400, errors.New("nil data"))
res, code, err := mockAcc.UpdateOne(nil, "id")
assert.Error(t, err)
assert.Equal(t, 400, code)
assert.Nil(t, res)
}
func TestDeleteOne_NilID(t *testing.T) {
mockAcc := new(MockWorkspaceAccessor)
mockAcc.On("DeleteOne", "").Return(nil, 400, errors.New("missing ID"))
res, code, err := mockAcc.DeleteOne("")
assert.Error(t, err)
assert.Equal(t, 400, code)
assert.Nil(t, res)
}

View File

@ -72,11 +72,9 @@ func (a *workspaceMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils
func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) { func (a *workspaceMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
filters := &dbs.Filters{ filters := &dbs.Filters{
Or: map[string][]dbs.Filter{ Or: map[string][]dbs.Filter{
"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: data.GetName() + "_workspace"}}, "abstractobject.name": {{Operator: dbs.LIKE.String(), Value: data.GetName() + "_workspace"}},
"abstractobject.creator_id": {{Operator: dbs.EQUAL.String(), Value: a.GetPeerID()}},
}, },
} }
// filters *dbs.Filters, word string, isDraft bool
res, _, err := a.Search(filters, "", true) // Search for the workspace res, _, err := a.Search(filters, "", true) // Search for the workspace
if err == nil && len(res) > 0 { // If the workspace already exists, return an error if err == nil && len(res) > 0 { // If the workspace already exists, return an error
return nil, 409, errors.New("a workspace with the same name already exists") return nil, 409, errors.New("a workspace with the same name already exists")

View File

@ -7,7 +7,6 @@ import (
"cloud.o-forge.io/core/oc-lib/config" "cloud.o-forge.io/core/oc-lib/config"
"cloud.o-forge.io/core/oc-lib/dbs/mongo" "cloud.o-forge.io/core/oc-lib/dbs/mongo"
"cloud.o-forge.io/core/oc-lib/logs"
beego "github.com/beego/beego/v2/server/web" beego "github.com/beego/beego/v2/server/web"
) )
@ -116,8 +115,8 @@ func (a *API) SubscribeRouter(infos []*beego.ControllerInfo) {
// CheckRemotePeer checks the state of a remote peer // CheckRemotePeer checks the state of a remote peer
func (a *API) CheckRemotePeer(url string) (State, map[string]int) { func (a *API) CheckRemotePeer(url string) (State, map[string]int) {
// Check if the database is up // Check if the database is up
var resp APIStatusResponse
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
var resp APIStatusResponse
b, err := caller.CallPost(url, "", map[string]interface{}{}) // Call the status endpoint of the peer b, err := caller.CallPost(url, "", map[string]interface{}{}) // Call the status endpoint of the peer
if err != nil { if err != nil {
return DEAD, map[string]int{} // If the peer is not reachable, return dead return DEAD, map[string]int{} // If the peer is not reachable, return dead
@ -136,7 +135,6 @@ func (a *API) CheckRemotePeer(url string) (State, map[string]int) {
// CheckRemoteAPIs checks the state of remote APIs from your proper OC // CheckRemoteAPIs checks the state of remote APIs from your proper OC
func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error) { func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error) {
// Check if the database is up // Check if the database is up
l := logs.GetLogger()
new := map[string]string{} new := map[string]string{}
caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller caller := NewHTTPCaller(map[DataType]map[METHOD]string{}) // Create a new http caller
code := 0 code := 0
@ -147,7 +145,6 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
var resp APIStatusResponse var resp APIStatusResponse
b, err := caller.CallGet("http://"+api.API()+":8080", "/oc/version/status") // Call the status endpoint of the remote API (standard OC status endpoint) b, err := caller.CallGet("http://"+api.API()+":8080", "/oc/version/status") // Call the status endpoint of the remote API (standard OC status endpoint)
if err != nil { if err != nil {
l.Error().Msg(api.String() + " not reachable")
state = REDUCED_SERVICE // If a remote API is not reachable, return reduced service state = REDUCED_SERVICE // If a remote API is not reachable, return reduced service
continue continue
} }
@ -164,7 +161,6 @@ func (a *API) CheckRemoteAPIs(apis []DataType) (State, map[string]string, error)
reachable = true // If the remote API is reachable, set reachable to true cause we are not dead reachable = true // If the remote API is reachable, set reachable to true cause we are not dead
} }
if !reachable { if !reachable {
l.Error().Msg("Peer check returned no answers")
state = DEAD // If no remote API is reachable, return dead, nobody is alive state = DEAD // If no remote API is reachable, return dead, nobody is alive
} }
if code > 0 { if code > 0 {

View File

@ -21,12 +21,6 @@ const (
WORKSPACE_HISTORY WORKSPACE_HISTORY
ORDER ORDER
PURCHASE_RESOURCE PURCHASE_RESOURCE
ADMIRALTY_SOURCE
ADMIRALTY_TARGET
ADMIRALTY_SECRET
ADMIRALTY_KUBECONFIG
ADMIRALTY_NODES
COMPUTE_UNITS
) )
var NOAPI = "" var NOAPI = ""
@ -36,11 +30,6 @@ var WORKFLOWAPI = "oc-workflow"
var WORKSPACEAPI = "oc-workspace" var WORKSPACEAPI = "oc-workspace"
var PEERSAPI = "oc-peer" var PEERSAPI = "oc-peer"
var DATACENTERAPI = "oc-datacenter" var DATACENTERAPI = "oc-datacenter"
var ADMIRALTY_SOURCEAPI = DATACENTERAPI + "/admiralty/source"
var ADMIRALTY_TARGETAPI = DATACENTERAPI + "/admiralty/target"
var ADMIRALTY_SECRETAPI = DATACENTERAPI + "/admiralty/secret"
var ADMIRALTY_KUBECONFIGAPI = DATACENTERAPI + "/admiralty/kubeconfig"
var ADMIRALTY_NODESAPI = DATACENTERAPI + "/admiralty/node"
// Bind the standard API name to the data type // Bind the standard API name to the data type
var DefaultAPI = [...]string{ var DefaultAPI = [...]string{
@ -61,12 +50,6 @@ var DefaultAPI = [...]string{
NOAPI, NOAPI,
NOAPI, NOAPI,
NOAPI, NOAPI,
ADMIRALTY_SOURCEAPI,
ADMIRALTY_TARGETAPI,
ADMIRALTY_SECRETAPI,
ADMIRALTY_KUBECONFIGAPI,
ADMIRALTY_NODESAPI,
DATACENTERAPI,
} }
// Bind the standard data name to the data type // Bind the standard data name to the data type
@ -88,12 +71,6 @@ var Str = [...]string{
"workspace_history", "workspace_history",
"order", "order",
"purchase_resource", "purchase_resource",
"admiralty_source",
"admiralty_target",
"admiralty_secret",
"admiralty_kubeconfig",
"admiralty_node",
"compute_units",
} }
func FromInt(i int) string { func FromInt(i int) string {
@ -114,5 +91,5 @@ func (d DataType) EnumIndex() int {
} }
func DataTypeList() []DataType { func DataTypeList() []DataType {
return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE, WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY, ORDER, PURCHASE_RESOURCE, ADMIRALTY_SOURCE, ADMIRALTY_TARGET, ADMIRALTY_SECRET, ADMIRALTY_KUBECONFIG, ADMIRALTY_NODES} return []DataType{DATA_RESOURCE, PROCESSING_RESOURCE, STORAGE_RESOURCE, COMPUTE_RESOURCE, WORKFLOW_RESOURCE, WORKFLOW, WORKFLOW_EXECUTION, WORKSPACE, PEER, COLLABORATIVE_AREA, RULE, BOOKING, WORKFLOW_HISTORY, WORKSPACE_HISTORY, ORDER, PURCHASE_RESOURCE}
} }

View File

@ -3,7 +3,6 @@ package tools
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -47,19 +46,11 @@ func ToMethod(str string) METHOD {
return GET return GET
} }
type HTTPCallerITF interface {
GetUrls() map[DataType]map[METHOD]string
CallGet(url string, subpath string, types ...string) ([]byte, error)
CallPost(url string, subpath string, body interface{}, types ...string) ([]byte, error)
CallDelete(url string, subpath string) ([]byte, error)
}
var HTTPCallerInstance = &HTTPCaller{} // Singleton instance of the HTTPCaller var HTTPCallerInstance = &HTTPCaller{} // Singleton instance of the HTTPCaller
type HTTPCaller struct { type HTTPCaller struct {
URLS map[DataType]map[METHOD]string // Map of the different methods and their urls URLS map[DataType]map[METHOD]string // Map of the different methods and their urls
Disabled bool // Disabled flag Disabled bool // Disabled flag
LastResults map[string]interface{} // Used to store information regarding the last execution of a given method on a given data type
} }
// NewHTTPCaller creates a new instance of the HTTP Caller // NewHTTPCaller creates a new instance of the HTTP Caller
@ -70,20 +61,6 @@ func NewHTTPCaller(urls map[DataType]map[METHOD]string) *HTTPCaller {
} }
} }
func (c *HTTPCaller) GetUrls() map[DataType]map[METHOD]string {
return c.URLS
}
// Creates a copy of the current caller, in order to have parallelized executions without race condition
func (c *HTTPCaller) DeepCopy(dst HTTPCaller) error {
bytes, err := json.Marshal(c)
if err != nil {
return err
}
return json.Unmarshal(bytes, &dst)
}
// CallGet calls the GET method on the HTTP server // CallGet calls the GET method on the HTTP server
func (caller *HTTPCaller) CallGet(url string, subpath string, types ...string) ([]byte, error) { func (caller *HTTPCaller) CallGet(url string, subpath string, types ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url+subpath, bytes.NewBuffer([]byte(""))) req, err := http.NewRequest(http.MethodGet, url+subpath, bytes.NewBuffer([]byte("")))
@ -99,33 +76,17 @@ func (caller *HTTPCaller) CallGet(url string, subpath string, types ...string) (
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
err = caller.StoreResp(resp) return io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return caller.LastResults["body"].([]byte), nil
} }
// CallPut calls the DELETE method on the HTTP server // CallPut calls the DELETE method on the HTTP server
func (caller *HTTPCaller) CallDelete(url string, subpath string) ([]byte, error) { func (caller *HTTPCaller) CallDelete(url string, subpath string) ([]byte, error) {
req, err := http.NewRequest("DELETE", url+subpath, nil) resp, err := http.NewRequest("DELETE", url+subpath, nil)
if err != nil { if err != nil || resp == nil || resp.Body == nil {
return nil, err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || req == nil || req.Body == nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
return io.ReadAll(resp.Body)
err = caller.StoreResp(resp)
if err != nil {
return nil, err
}
return caller.LastResults["body"].([]byte), nil
} }
// CallPost calls the POST method on the HTTP server // CallPost calls the POST method on the HTTP server
@ -144,12 +105,7 @@ func (caller *HTTPCaller) CallPost(url string, subpath string, body interface{},
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
err = caller.StoreResp(resp) return io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return caller.LastResults["body"].([]byte), nil
} }
// CallPost calls the POST method on the HTTP server // CallPost calls the POST method on the HTTP server
@ -167,12 +123,7 @@ func (caller *HTTPCaller) CallPut(url string, subpath string, body map[string]in
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
err = caller.StoreResp(resp) return io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return caller.LastResults["body"].([]byte), nil
} }
// CallRaw calls the Raw method on the HTTP server // CallRaw calls the Raw method on the HTTP server
@ -192,12 +143,7 @@ func (caller *HTTPCaller) CallRaw(method string, url string, subpath string,
req.AddCookie(c) req.AddCookie(c)
} }
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) return client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
} }
// CallRaw calls the Raw method on the HTTP server // CallRaw calls the Raw method on the HTTP server
@ -217,17 +163,3 @@ func (caller *HTTPCaller) CallForm(method string, url string, subpath string,
client := &http.Client{} client := &http.Client{}
return client.Do(req) return client.Do(req)
} }
func (caller *HTTPCaller) StoreResp(resp *http.Response) error {
caller.LastResults = make(map[string]interface{})
caller.LastResults["header"] = resp.Header
caller.LastResults["code"] = resp.StatusCode
data, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading the body of the last request")
return err
}
caller.LastResults["body"] = data
return nil
}

View File

@ -1,227 +0,0 @@
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")
}
}