From 48299810e04a78a680554bbf0c39ad598317633d Mon Sep 17 00:00:00 2001 From: mr Date: Mon, 16 Jun 2025 11:24:39 +0200 Subject: [PATCH] draft test --- go.mod | 3 +- go.sum | 4 + models/models.go | 8 +- models/order/order.go | 4 +- models/order/tests/order_test.go | 0 models/peer/peer_cache.go | 12 +- models/peer/peer_mongo_accessor.go | 12 +- models/peer/tests/peer_cache_test.go | 100 ++++++++ models/peer/tests/peer_test.go | 127 +++++++++++ models/resources/compute.go | 0 models/resources/data.go | 0 models/resources/interfaces.go | 0 models/resources/models.go | 0 models/resources/priced_resource.go | 0 models/resources/processing.go | 0 models/resources/resource.go | 0 models/resources/resource_accessor.go | 0 models/resources/storage.go | 0 models/resources/workflow.go | 0 models/tests/models_test.go | 38 ++++ models/utils/abstracts.go | 0 models/utils/common.go | 0 models/utils/interfaces.go | 0 models/utils/tests/abstracts_test.go | 128 +++++++++++ models/utils/tests/common_test.go | 168 ++++++++++++++ .../tests/workflow_scheduler_test.go | 149 ++++++++++++ .../workflow_execution/tests/workflow_test.go | 154 +++++++++++++ .../workflow_execution/workflow_execution.go | 0 .../workflow_execution_mongo_accessor.go | 28 +-- .../workflow_execution/workflow_scheduler.go | 16 +- .../tests/workspace_mongo_accessor_test.go | 215 ++++++++++++++++++ tools/remote_caller.go | 23 +- 32 files changed, 1142 insertions(+), 47 deletions(-) mode change 100644 => 100755 go.mod mode change 100644 => 100755 go.sum create mode 100644 models/order/tests/order_test.go create mode 100644 models/peer/tests/peer_cache_test.go create mode 100644 models/peer/tests/peer_test.go mode change 100644 => 100755 models/resources/compute.go mode change 100644 => 100755 models/resources/data.go mode change 100644 => 100755 models/resources/interfaces.go mode change 100644 => 100755 models/resources/models.go mode change 100644 => 100755 models/resources/priced_resource.go mode change 100644 => 100755 models/resources/processing.go mode change 100644 => 100755 models/resources/resource.go mode change 100644 => 100755 models/resources/resource_accessor.go mode change 100644 => 100755 models/resources/storage.go mode change 100644 => 100755 models/resources/workflow.go create mode 100644 models/tests/models_test.go mode change 100644 => 100755 models/utils/abstracts.go mode change 100644 => 100755 models/utils/common.go mode change 100644 => 100755 models/utils/interfaces.go create mode 100644 models/utils/tests/abstracts_test.go create mode 100644 models/utils/tests/common_test.go create mode 100644 models/workflow_execution/tests/workflow_scheduler_test.go create mode 100755 models/workflow_execution/tests/workflow_test.go mode change 100644 => 100755 models/workflow_execution/workflow_execution.go mode change 100644 => 100755 models/workflow_execution/workflow_execution_mongo_accessor.go mode change 100644 => 100755 models/workflow_execution/workflow_scheduler.go create mode 100644 models/workspace/tests/workspace_mongo_accessor_test.go diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 index 514863a..5232dad --- a/go.mod +++ b/go.mod @@ -10,12 +10,13 @@ require ( github.com/nats-io/nats.go v1.37.0 github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.33.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 ) require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect ) require ( diff --git a/go.sum b/go.sum old mode 100644 new mode 100755 index 923af6d..ce3588c --- a/go.sum +++ b/go.sum @@ -106,9 +106,13 @@ 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/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.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.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.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/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= diff --git a/models/models.go b/models/models.go index a52574a..986ce41 100644 --- a/models/models.go +++ b/models/models.go @@ -21,7 +21,7 @@ import ( This package contains the models used in the application It's used to create the models dynamically */ -var models = map[string]func() utils.DBObject{ +var ModelsCatalog = map[string]func() utils.DBObject{ tools.WORKFLOW_RESOURCE.String(): func() utils.DBObject { return &resource.WorkflowResource{} }, tools.DATA_RESOURCE.String(): func() utils.DBObject { return &resource.DataResource{} }, tools.COMPUTE_RESOURCE.String(): func() utils.DBObject { return &resource.ComputeResource{} }, @@ -43,8 +43,8 @@ var models = map[string]func() utils.DBObject{ // Model returns the model object based on the model type func Model(model int) utils.DBObject { log := logs.GetLogger() - if _, ok := models[tools.FromInt(model)]; ok { - return models[tools.FromInt(model)]() + if _, ok := ModelsCatalog[tools.FromInt(model)]; ok { + return ModelsCatalog[tools.FromInt(model)]() } log.Error().Msg("Can't find model " + tools.FromInt(model) + ".") return nil @@ -53,7 +53,7 @@ func Model(model int) utils.DBObject { // GetModelsNames returns the names of the models func GetModelsNames() []string { names := []string{} - for name := range models { + for name := range ModelsCatalog { names = append(names, name) } return names diff --git a/models/order/order.go b/models/order/order.go index 63389bc..a6c6e8b 100644 --- a/models/order/order.go +++ b/models/order/order.go @@ -133,7 +133,7 @@ func (o *Order) draftStoreFromModel(scheduler *workflow_execution.WorkflowSchedu o.SubOrders[peerOrder.GetID()] = peerOrder } // search an order with same user name and same session id - err := o.sumUpBill(request) + err := o.SumUpBill(request) if err != nil { 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 } -func (d *Order) sumUpBill(request *tools.APIRequest) error { +func (d *Order) SumUpBill(request *tools.APIRequest) error { for _, b := range d.SubOrders { err := b.SumUpBill(request) if err != nil { diff --git a/models/order/tests/order_test.go b/models/order/tests/order_test.go new file mode 100644 index 0000000..e69de29 diff --git a/models/peer/peer_cache.go b/models/peer/peer_cache.go index 43c1a14..963ef9e 100644 --- a/models/peer/peer_cache.go +++ b/models/peer/peer_cache.go @@ -50,9 +50,9 @@ func (p *PeerCache) checkPeerStatus(peerID string, appName string) (*Peer, bool) // 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, - dt tools.DataType, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) (*PeerExecution, error) { - fmt.Println("Launching peer execution on", caller.URLS, dt, method) - methods := caller.URLS[dt] // Get the methods url of the data type + dt tools.DataType, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) (*PeerExecution, error) { + fmt.Println("Launching peer execution on", caller.GetUrls(), dt, method) + methods := caller.GetUrls()[dt] // Get the methods url of the data type if m, ok := methods[method]; !ok || m == "" { return nil, errors.New("Requested method " + method.String() + " not declared in HTTPCaller") } @@ -83,14 +83,14 @@ func (p *PeerCache) LaunchPeerExecution(peerID string, dataID string, mypeer.FailedExecution = []PeerExecution{} // Reset the failed executions list NewShallowAccessor().UpdateOne(mypeer, peerID) // Update the peer in the db 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 -func (p *PeerCache) exec(url string, method tools.METHOD, body interface{}, caller *tools.HTTPCaller) error { +func (p *PeerCache) Exec(url string, method tools.METHOD, body interface{}, caller tools.HTTPCallerITF) error { var b []byte var err error if method == tools.POST { // Execute the POST method if it's a POST method diff --git a/models/peer/peer_mongo_accessor.go b/models/peer/peer_mongo_accessor.go index ad24b1f..0c974d3 100644 --- a/models/peer/peer_mongo_accessor.go +++ b/models/peer/peer_mongo_accessor.go @@ -11,13 +11,13 @@ import ( type peerMongoAccessor struct { utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller) - overrideAuth bool + OverrideAuth bool } // New creates a new instance of the peerMongoAccessor func NewShallowAccessor() *peerMongoAccessor { return &peerMongoAccessor{ - overrideAuth: true, + OverrideAuth: true, AbstractAccessor: utils.AbstractAccessor{ Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Type: tools.PEER, @@ -27,7 +27,7 @@ func NewShallowAccessor() *peerMongoAccessor { func NewAccessor(request *tools.APIRequest) *peerMongoAccessor { return &peerMongoAccessor{ - overrideAuth: false, + OverrideAuth: false, AbstractAccessor: utils.AbstractAccessor{ Logger: logs.CreateLogger(tools.PEER.String()), // Create a logger with the data type Request: request, @@ -37,7 +37,7 @@ func NewAccessor(request *tools.APIRequest) *peerMongoAccessor { } 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) { - return utils.GenericSearch[*Peer](filters, search, wfa.getDefaultFilter(search), + return utils.GenericSearch[*Peer](filters, search, wfa.GetDefaultFilter(search), func(d utils.DBObject) utils.ShallowDBObject { return d }, 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 { return &dbs.Filters{ Or: map[string][]dbs.Filter{ // search by name if no filters are provided diff --git a/models/peer/tests/peer_cache_test.go b/models/peer/tests/peer_cache_test.go new file mode 100644 index 0000000..177d727 --- /dev/null +++ b/models/peer/tests/peer_cache_test.go @@ -0,0 +1,100 @@ +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) +} diff --git a/models/peer/tests/peer_test.go b/models/peer/tests/peer_test.go new file mode 100644 index 0000000..8f4861f --- /dev/null +++ b/models/peer/tests/peer_test.go @@ -0,0 +1,127 @@ +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) +} diff --git a/models/resources/compute.go b/models/resources/compute.go old mode 100644 new mode 100755 diff --git a/models/resources/data.go b/models/resources/data.go old mode 100644 new mode 100755 diff --git a/models/resources/interfaces.go b/models/resources/interfaces.go old mode 100644 new mode 100755 diff --git a/models/resources/models.go b/models/resources/models.go old mode 100644 new mode 100755 diff --git a/models/resources/priced_resource.go b/models/resources/priced_resource.go old mode 100644 new mode 100755 diff --git a/models/resources/processing.go b/models/resources/processing.go old mode 100644 new mode 100755 diff --git a/models/resources/resource.go b/models/resources/resource.go old mode 100644 new mode 100755 diff --git a/models/resources/resource_accessor.go b/models/resources/resource_accessor.go old mode 100644 new mode 100755 diff --git a/models/resources/storage.go b/models/resources/storage.go old mode 100644 new mode 100755 diff --git a/models/resources/workflow.go b/models/resources/workflow.go old mode 100644 new mode 100755 diff --git a/models/tests/models_test.go b/models/tests/models_test.go new file mode 100644 index 0000000..aa558cf --- /dev/null +++ b/models/tests/models_test.go @@ -0,0 +1,38 @@ +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) + } +} diff --git a/models/utils/abstracts.go b/models/utils/abstracts.go old mode 100644 new mode 100755 diff --git a/models/utils/common.go b/models/utils/common.go old mode 100644 new mode 100755 diff --git a/models/utils/interfaces.go b/models/utils/interfaces.go old mode 100644 new mode 100755 diff --git a/models/utils/tests/abstracts_test.go b/models/utils/tests/abstracts_test.go new file mode 100644 index 0000000..83c9c33 --- /dev/null +++ b/models/utils/tests/abstracts_test.go @@ -0,0 +1,128 @@ +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()) +} diff --git a/models/utils/tests/common_test.go b/models/utils/tests/common_test.go new file mode 100644 index 0000000..cc15516 --- /dev/null +++ b/models/utils/tests/common_test.go @@ -0,0 +1,168 @@ +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()) +} diff --git a/models/workflow_execution/tests/workflow_scheduler_test.go b/models/workflow_execution/tests/workflow_scheduler_test.go new file mode 100644 index 0000000..d4ae87c --- /dev/null +++ b/models/workflow_execution/tests/workflow_scheduler_test.go @@ -0,0 +1,149 @@ +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... diff --git a/models/workflow_execution/tests/workflow_test.go b/models/workflow_execution/tests/workflow_test.go new file mode 100755 index 0000000..4c93c24 --- /dev/null +++ b/models/workflow_execution/tests/workflow_test.go @@ -0,0 +1,154 @@ +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") +} diff --git a/models/workflow_execution/workflow_execution.go b/models/workflow_execution/workflow_execution.go old mode 100644 new mode 100755 diff --git a/models/workflow_execution/workflow_execution_mongo_accessor.go b/models/workflow_execution/workflow_execution_mongo_accessor.go old mode 100644 new mode 100755 index 9b38913..ecb5c9a --- a/models/workflow_execution/workflow_execution_mongo_accessor.go +++ b/models/workflow_execution/workflow_execution_mongo_accessor.go @@ -11,13 +11,13 @@ import ( "cloud.o-forge.io/core/oc-lib/tools" ) -type workflowExecutionMongoAccessor struct { +type WorkflowExecutionMongoAccessor struct { utils.AbstractAccessor shallow bool } -func newShallowAccessor(request *tools.APIRequest) *workflowExecutionMongoAccessor { - return &workflowExecutionMongoAccessor{ +func newShallowAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor { + return &WorkflowExecutionMongoAccessor{ shallow: true, AbstractAccessor: utils.AbstractAccessor{ 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 { - return &workflowExecutionMongoAccessor{ +func NewAccessor(request *tools.APIRequest) *WorkflowExecutionMongoAccessor { + return &WorkflowExecutionMongoAccessor{ shallow: false, AbstractAccessor: utils.AbstractAccessor{ 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") } -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 { return nil, 400, errors.New("state is required") } @@ -50,15 +50,15 @@ func (wfa *workflowExecutionMongoAccessor) UpdateOne(set utils.DBObject, id stri 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") } -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") } -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) { now := time.Now() now = now.Add(time.Second * -60) @@ -74,15 +74,15 @@ func (a *workflowExecutionMongoAccessor) LoadOne(id string) (utils.DBObject, int }, 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) } -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) } -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 { now := time.Now() now = now.Add(time.Second * -60) @@ -99,7 +99,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{ Or: map[string][]dbs.Filter{ // filter by name if no filters are provided "abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search + "_execution"}}, diff --git a/models/workflow_execution/workflow_scheduler.go b/models/workflow_execution/workflow_scheduler.go old mode 100644 new mode 100755 index 23e87d7..6b71cda --- a/models/workflow_execution/workflow_scheduler.go +++ b/models/workflow_execution/workflow_scheduler.go @@ -71,7 +71,7 @@ func (ws *WorkflowSchedule) CheckBooking(wfID string, request *tools.APIRequest) 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" } - execs, err := ws.getExecutions(wf) + execs, err := ws.GetExecutions(wf) if err != nil { return false, wf, []*WorkflowExecution{}, []*booking.Booking{}, err } @@ -96,7 +96,7 @@ func (ws *WorkflowSchedule) CheckBooking(wfID string, request *tools.APIRequest) 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) { +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) @@ -157,9 +157,9 @@ func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (* for _, booking := range bookings { go ws.BookExecs(booking, request, errCh, &m) } - + for i := 0; i < len(bookings); i++ { - if err := <- errCh ; err != nil { + if err := <-errCh; err != nil { return ws, wf, executions, errors.New("could not launch the peer execution : " + fmt.Sprintf("%v", err)) } } @@ -189,7 +189,7 @@ func (ws *WorkflowSchedule) BookExecs(booking *booking.Booking, request *tools.A _, err = (&peer.Peer{}).LaunchPeerExecution(booking.DestPeerID, "", tools.BOOKING, tools.POST, booking.Serialize(booking), &c) - + if err != nil { errCh <- err return @@ -210,9 +210,9 @@ VERIFY THAT WE HANDLE DIFFERENCE BETWEEN LOCATION TIME && BOOKING * getExecutions is a function that returns the executions of a workflow * 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{} - dates, err := ws.getDates() + dates, err := ws.GetDates() if err != nil { return workflows_executions, err } @@ -233,7 +233,7 @@ func (ws *WorkflowSchedule) getExecutions(workflow *workflow.Workflow) ([]*Workf return workflows_executions, nil } -func (ws *WorkflowSchedule) getDates() ([]Schedule, error) { +func (ws *WorkflowSchedule) GetDates() ([]Schedule, error) { schedule := []Schedule{} if len(ws.Cron) > 0 { // if cron is set then end date should be set if ws.End == nil { diff --git a/models/workspace/tests/workspace_mongo_accessor_test.go b/models/workspace/tests/workspace_mongo_accessor_test.go new file mode 100644 index 0000000..d978887 --- /dev/null +++ b/models/workspace/tests/workspace_mongo_accessor_test.go @@ -0,0 +1,215 @@ +// 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) +} diff --git a/tools/remote_caller.go b/tools/remote_caller.go index c90ab0a..6e3fb16 100644 --- a/tools/remote_caller.go +++ b/tools/remote_caller.go @@ -47,12 +47,19 @@ func ToMethod(str string) METHOD { 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 type HTTPCaller struct { - URLS map[DataType]map[METHOD]string // Map of the different methods and their urls - 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 + URLS map[DataType]map[METHOD]string // Map of the different methods and their urls + 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 @@ -63,8 +70,12 @@ func NewHTTPCaller(urls map[DataType]map[METHOD]string) *HTTPCaller { } } -// Creates a copy of the current caller, in order to have parallelized executions without race condition -func (c* HTTPCaller) DeepCopy(dst HTTPCaller) error { +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 @@ -219,4 +230,4 @@ func (caller *HTTPCaller) StoreResp(resp *http.Response) error { caller.LastResults["body"] = data return nil -} \ No newline at end of file +}