From e4277395b2afd77640ad03f0f555f01a3f4d2725 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 11:18:11 +0300 Subject: [PATCH 01/26] ``` Add tests for NewHeartbeat and Update functions ``` This commit adds tests for the `NewHeartbeat` and `Update` functions in the `agent` package. The `NewHeartbeat` test checks if the `NewHeartbeat` function correctly initializes a new heartbeat with the given name, service type, and interval. It also verifies the initial status and info of the heartbeat. Signed-off-by: SammyOina --- pkg/agent/heartbeat_test.go | 62 +++++++++++++++++++++++++++++++++++++ pkg/agent/service.go | 4 +-- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 pkg/agent/heartbeat_test.go diff --git a/pkg/agent/heartbeat_test.go b/pkg/agent/heartbeat_test.go new file mode 100644 index 0000000..26a05bb --- /dev/null +++ b/pkg/agent/heartbeat_test.go @@ -0,0 +1,62 @@ +package agent + +import ( + "testing" + "time" +) + +func TestNewHeartbeat(t *testing.T) { + name := "TestService" + serviceType := "TestType" + interval := 2 * time.Second + + heartbeat := NewHeartbeat(name, serviceType, interval) + + // Check initial status and info + info := heartbeat.Info() + if info.Name != name { + t.Errorf("Expected name to be %s, but got %s", name, info.Name) + } + if info.Type != serviceType { + t.Errorf("Expected type to be %s, but got %s", serviceType, info.Type) + } + if info.Status != online { + t.Errorf("Expected initial status to be %s, but got %s", online, info.Status) + } +} + +func TestHeartbeat_Update(t *testing.T) { + name := "TestService" + serviceType := "TestType" + interval := 2 * time.Second + + heartbeat := NewHeartbeat(name, serviceType, interval) + + // Sleep for more than the interval to simulate an update + time.Sleep(3 * time.Second) + + heartbeat.Update() + + // Check if the status has been updated to online + info := heartbeat.Info() + if info.Status != online { + t.Errorf("Expected status to be %s, but got %s", online, info.Status) + } +} + +func TestHeartbeat_StatusOffline(t *testing.T) { + name := "TestService" + serviceType := "TestType" + interval := 2 * time.Second + + heartbeat := NewHeartbeat(name, serviceType, interval) + + // Sleep for more than two intervals to simulate offline status + time.Sleep(5 * time.Second) + + // Check if the status has been updated to offline + info := heartbeat.Info() + if info.Status != offline { + t.Errorf("Expected status to be %s, but got %s", offline, info.Status) + } +} diff --git a/pkg/agent/service.go b/pkg/agent/service.go index 98b445b..4f43f64 100644 --- a/pkg/agent/service.go +++ b/pkg/agent/service.go @@ -126,8 +126,8 @@ func (ag *agent) handle(ctx context.Context, pub messaging.Publisher, logger log sub := msg.Channel tok := strings.Split(sub, ".") if len(tok) < 3 { - ag.logger.Error(fmt.Sprintf("Failed: Subject has incorrect length %s", sub)) - return fmt.Errorf("Failed: Subject has incorrect length %s", sub) + ag.logger.Error(fmt.Sprintf("failed: subject has incorrect length %s", sub)) + return fmt.Errorf("failed: subject has incorrect length %s", sub) } svcname := tok[1] svctype := tok[2] From 1760a964d2c597c0622bd57afa5afb09a2ad72f2 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 11:49:06 +0300 Subject: [PATCH 02/26] extract constants Signed-off-by: SammyOina --- pkg/agent/heartbeat_test.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pkg/agent/heartbeat_test.go b/pkg/agent/heartbeat_test.go index 26a05bb..5b603fa 100644 --- a/pkg/agent/heartbeat_test.go +++ b/pkg/agent/heartbeat_test.go @@ -5,11 +5,13 @@ import ( "time" ) -func TestNewHeartbeat(t *testing.T) { - name := "TestService" - serviceType := "TestType" - interval := 2 * time.Second +const ( + name = "TestService" + serviceType = "TestType" + interval = 2 * time.Second +) +func TestNewHeartbeat(t *testing.T) { heartbeat := NewHeartbeat(name, serviceType, interval) // Check initial status and info @@ -26,10 +28,6 @@ func TestNewHeartbeat(t *testing.T) { } func TestHeartbeat_Update(t *testing.T) { - name := "TestService" - serviceType := "TestType" - interval := 2 * time.Second - heartbeat := NewHeartbeat(name, serviceType, interval) // Sleep for more than the interval to simulate an update @@ -45,10 +43,6 @@ func TestHeartbeat_Update(t *testing.T) { } func TestHeartbeat_StatusOffline(t *testing.T) { - name := "TestService" - serviceType := "TestType" - interval := 2 * time.Second - heartbeat := NewHeartbeat(name, serviceType, interval) // Sleep for more than two intervals to simulate offline status From 98d44f3df8e4aa82ac79ac88e6db0d80625c321b Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 15:57:31 +0300 Subject: [PATCH 03/26] Add bootstrap tests. Fix error when writing to TOML file The commit fixes an error that occurs when writing to a TOML file. The error is wrapped with a custom error message. This ensures that the error is properly handled and provides more context to the developer. The commit also includes changes to the import statements in the bootstrap.go file. Signed-off-by: SammyOina --- pkg/agent/config.go | 4 +- pkg/bootstrap/bootstrap.go | 40 +++++----- pkg/bootstrap/bootstrap_test.go | 136 ++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 pkg/bootstrap/bootstrap_test.go diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 36b495c..0f61aa2 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -14,6 +14,8 @@ import ( "github.com/pelletier/go-toml" ) +var ErrWritingToml = errors.New("error writing to toml file") + type ServerConfig struct { Port string `toml:"port" json:"port"` BrokerURL string `toml:"broker_url" json:"broker_url"` @@ -89,7 +91,7 @@ func SaveConfig(c Config) error { return errors.New(fmt.Sprintf("Error reading config file: %s", err)) } if err := os.WriteFile(c.File, b, 0644); err != nil { - return errors.New(fmt.Sprintf("Error writing toml: %s", err)) + return errors.Wrap(ErrWritingToml, err) } return nil } diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go index a24ceef..a805fdb 100644 --- a/pkg/bootstrap/bootstrap.go +++ b/pkg/bootstrap/bootstrap.go @@ -25,6 +25,11 @@ import ( const exportConfigFile = "/configs/export/config.toml" +var ( + errInvalidBootstrapRetriesValue = errors.New("invalid BOOTSTRAP_RETRIES value") + errInvalidBootstrapRetryDelay = errors.New("invalid BOOTSTRAP_RETRY_DELAY_SECONDS value") +) + // Config represents the parameters for bootstrapping. type Config struct { URL string @@ -46,20 +51,20 @@ type ConfigContent struct { } type deviceConfig struct { - MainfluxID string `json:"mainflux_id"` - MainfluxKey string `json:"mainflux_key"` - MainfluxChannels []bootstrap.Channel `json:"mainflux_channels"` - ClientKey string `json:"client_key"` - ClientCert string `json:"client_cert"` - CaCert string `json:"ca_cert"` - SvcsConf ServicesConfig `json:"-"` + ThingID string `json:"thing_id"` + ThingKey string `json:"thing_key"` + Channels []bootstrap.Channel `json:"channels"` + ClientKey string `json:"client_key"` + ClientCert string `json:"client_cert"` + CaCert string `json:"ca_cert"` + SvcsConf ServicesConfig `json:"-"` } // Bootstrap - Retrieve device config. func Bootstrap(cfg Config, logger log.Logger, file string) error { retries, err := strconv.ParseUint(cfg.Retries, 10, 64) if err != nil { - return errors.New(fmt.Sprintf("Invalid BOOTSTRAP_RETRIES value: %s", err)) + return errors.Wrap(errInvalidBootstrapRetriesValue, err) } if retries == 0 { @@ -69,7 +74,7 @@ func Bootstrap(cfg Config, logger log.Logger, file string) error { retryDelaySec, err := strconv.ParseUint(cfg.RetryDelaySec, 10, 64) if err != nil { - return errors.New(fmt.Sprintf("Invalid BOOTSTRAP_RETRY_DELAY_SECONDS value: %s", err)) + return errors.Wrap(errInvalidBootstrapRetryDelay, err) } logger.Info(fmt.Sprintf("Requesting config for %s from %s", cfg.ID, cfg.URL)) @@ -91,15 +96,15 @@ func Bootstrap(cfg Config, logger log.Logger, file string) error { } } - if len(dc.MainfluxChannels) < 2 { + if len(dc.Channels) < 2 { return agent.ErrMalformedEntity } - ctrlChan := dc.MainfluxChannels[0].ID - dataChan := dc.MainfluxChannels[1].ID - if dc.MainfluxChannels[0].Metadata["type"] == "data" { - ctrlChan = dc.MainfluxChannels[1].ID - dataChan = dc.MainfluxChannels[0].ID + ctrlChan := dc.Channels[0].ID + dataChan := dc.Channels[1].ID + if dc.Channels[0].Metadata["type"] == "data" { + ctrlChan = dc.Channels[1].ID + dataChan = dc.Channels[0].ID } sc := dc.SvcsConf.Agent.Server @@ -111,8 +116,8 @@ func Bootstrap(cfg Config, logger log.Logger, file string) error { lc := dc.SvcsConf.Agent.Log mc := dc.SvcsConf.Agent.MQTT - mc.Password = dc.MainfluxKey - mc.Username = dc.MainfluxID + mc.Password = dc.ThingKey + mc.Username = dc.ThingID mc.ClientCert = dc.ClientCert mc.ClientKey = dc.ClientKey mc.CaCert = dc.CaCert @@ -215,7 +220,6 @@ func getConfig(bsID, bsKey, bsSvrURL string, skipTLS bool, logger log.Logger) (d if err := json.Unmarshal([]byte(body), &h); err != nil { return deviceConfig{}, err } - fmt.Println(h.Content) sc := ServicesConfig{} if err := json.Unmarshal([]byte(h.Content), &sc); err != nil { return deviceConfig{}, err diff --git a/pkg/bootstrap/bootstrap_test.go b/pkg/bootstrap/bootstrap_test.go new file mode 100644 index 0000000..de7fe19 --- /dev/null +++ b/pkg/bootstrap/bootstrap_test.go @@ -0,0 +1,136 @@ +package bootstrap + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/mainflux/agent/pkg/agent" + "github.com/mainflux/mainflux/logger" + "github.com/mainflux/mainflux/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestBootstrap(t *testing.T) { + // Create a mock HTTP server to handle requests from the getConfig function. + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Thing mockKey" && r.Header.Get("Authorization") != "Thing invalidChannels" { + http.Error(w, "Invalid authorization header", http.StatusUnauthorized) + return + } + if r.Header.Get("Authorization") == "Thing invalidChannels" { + // Simulate a malformed response. + resp := ` + { + "thing_id": "e22c383a-d2ab-47c1-89cd-903955da993d", + "thing_key": "fc987711-1828-461b-aa4b-16d5b2c642fe", + "channels": [ + { + "id": "fa5f9ba8-a1fc-4380-9edb-d0c23eaa24ec", + "name": "control-channel", + "metadata": { + "type": "control" + } + } + ], + "content": "{\"agent\":{\"edgex\":{\"url\":\"http://localhost:48090/api/v1/\"},\"heartbeat\":{\"interval\":\"30s\"},\"log\":{\"level\":\"debug\"},\"mqtt\":{\"mtls\":false,\"qos\":0,\"retain\":false,\"skip_tls_ver\":true,\"url\":\"tcp://mainflux-domain.com:1883\"},\"server\":{\"nats_url\":\"localhost:4222\",\"port\":\"9000\"},\"terminal\":{\"session_timeout\":\"30s\"}},\"export\":{\"exp\":{\"cache_db\":\"0\",\"cache_pass\":\"\",\"cache_url\":\"localhost:6379\",\"log_level\":\"debug\",\"nats\":\"nats://localhost:4222\",\"port\":\"8172\"},\"mqtt\":{\"ca_path\":\"ca.crt\",\"cert_path\":\"thing.crt\",\"channel\":\"\",\"host\":\"tcp://mainflux-domain.com:1883\",\"mtls\":false,\"password\":\"\",\"priv_key_path\":\"thing.key\",\"qos\":0,\"retain\":false,\"skip_tls_ver\":false,\"username\":\"\"},\"routes\":[{\"mqtt_topic\":\"\",\"nats_topic\":\"channels\",\"subtopic\":\"\",\"type\":\"mfx\",\"workers\":10},{\"mqtt_topic\":\"\",\"nats_topic\":\"export\",\"subtopic\":\"\",\"type\":\"default\",\"workers\":10}]}}" + } + ` + w.WriteHeader(http.StatusOK) + io.WriteString(w, resp) + return + } + // Simulate a successful response. + resp := ` + { + "thing_id": "e22c383a-d2ab-47c1-89cd-903955da993d", + "thing_key": "fc987711-1828-461b-aa4b-16d5b2c642fe", + "channels": [ + { + "id": "fa5f9ba8-a1fc-4380-9edb-d0c23eaa24ec", + "name": "control-channel", + "metadata": { + "type": "control" + } + }, + { + "id": "24e5473e-3cbe-43d9-8a8b-a725ff918c0e", + "name": "data-channel", + "metadata": { + "type": "data" + } + }, + { + "id": "1eac45c2-0f72-4089-b255-ebd2e5732bbb", + "name": "export-channel", + "metadata": { + "type": "export" + } + } + ], + "content": "{\"agent\":{\"edgex\":{\"url\":\"http://localhost:48090/api/v1/\"},\"heartbeat\":{\"interval\":\"30s\"},\"log\":{\"level\":\"debug\"},\"mqtt\":{\"mtls\":false,\"qos\":0,\"retain\":false,\"skip_tls_ver\":true,\"url\":\"tcp://mainflux-domain.com:1883\"},\"server\":{\"nats_url\":\"localhost:4222\",\"port\":\"9000\"},\"terminal\":{\"session_timeout\":\"30s\"}},\"export\":{\"exp\":{\"cache_db\":\"0\",\"cache_pass\":\"\",\"cache_url\":\"localhost:6379\",\"log_level\":\"debug\",\"nats\":\"nats://localhost:4222\",\"port\":\"8172\"},\"mqtt\":{\"ca_path\":\"ca.crt\",\"cert_path\":\"thing.crt\",\"channel\":\"\",\"host\":\"tcp://mainflux-domain.com:1883\",\"mtls\":false,\"password\":\"\",\"priv_key_path\":\"thing.key\",\"qos\":0,\"retain\":false,\"skip_tls_ver\":false,\"username\":\"\"},\"routes\":[{\"mqtt_topic\":\"\",\"nats_topic\":\"channels\",\"subtopic\":\"\",\"type\":\"mfx\",\"workers\":10},{\"mqtt_topic\":\"\",\"nats_topic\":\"export\",\"subtopic\":\"\",\"type\":\"default\",\"workers\":10}]}}" + } + ` + w.WriteHeader(http.StatusOK) + io.WriteString(w, resp) + })) + defer mockServer.Close() + mockLogger := logger.NewMock() + tests := []struct { + name string + config Config + file string + expectedErr error + }{ + { + name: "invalid retries type", + config: Config{Retries: "invalid"}, + expectedErr: errInvalidBootstrapRetriesValue, + }, + { + name: "zero retires", + config: Config{Retries: "0"}, + expectedErr: nil, + }, + { + name: "invalid retry delay", + config: Config{Retries: "1", RetryDelaySec: "e"}, + expectedErr: errInvalidBootstrapRetryDelay, + }, + { + name: "authorization error", + config: Config{Retries: "1", RetryDelaySec: "1", URL: mockServer.URL, Key: "wrongKey"}, + expectedErr: nil, + }, + { + name: "malformed channels", + config: Config{Retries: "1", RetryDelaySec: "1", URL: mockServer.URL, Key: "invalidChannels"}, + expectedErr: errors.ErrMalformedEntity, + }, + { + name: "successful configuration", + config: Config{Retries: "1", RetryDelaySec: "1", URL: mockServer.URL, Key: "mockKey"}, + expectedErr: agent.ErrWritingToml, + }, + { + name: "successful configuration", + config: Config{Retries: "1", RetryDelaySec: "1", URL: mockServer.URL, Key: "mockKey"}, + expectedErr: nil, + file: "config.toml", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := Bootstrap(test.config, mockLogger, test.file) + assert.True(t, errors.Contains(err, test.expectedErr), fmt.Sprintf("expected %v got %v", test.expectedErr, err)) + }) + } + // cleanup. + t.Cleanup(func() { + os.Remove("config.toml") + }) +} From 2c009583cec5ccd749bdb0650ac495667c831e7f Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 16:03:34 +0300 Subject: [PATCH 04/26] Fix error handling in bootstrap_test.go The commit fixes error handling in the bootstrap_test.go file by checking for an error when writing the response body. This ensures that any errors are properly handled and reported. Signed-off-by: SammyOina --- pkg/bootstrap/bootstrap_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/bootstrap/bootstrap_test.go b/pkg/bootstrap/bootstrap_test.go index de7fe19..8fd85fb 100644 --- a/pkg/bootstrap/bootstrap_test.go +++ b/pkg/bootstrap/bootstrap_test.go @@ -40,7 +40,9 @@ func TestBootstrap(t *testing.T) { } ` w.WriteHeader(http.StatusOK) - io.WriteString(w, resp) + if _, err := io.WriteString(w, resp); err != nil { + t.Errorf(err.Error()) + } return } // Simulate a successful response. @@ -75,7 +77,9 @@ func TestBootstrap(t *testing.T) { } ` w.WriteHeader(http.StatusOK) - io.WriteString(w, resp) + if _, err := io.WriteString(w, resp); err != nil { + t.Errorf(err.Error()) + } })) defer mockServer.Close() mockLogger := logger.NewMock() From d6c94d45faafe62cb4bdddb1eb7eeed19d494a8a Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 16:26:32 +0300 Subject: [PATCH 05/26] Fix context bug in NewHeartbeat function to allow graceful stop to listen function Signed-off-by: SammyOina --- pkg/agent/heartbeat.go | 16 ++++++++++------ pkg/agent/heartbeat_test.go | 19 ++++++++++++++++--- pkg/agent/service.go | 2 +- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/pkg/agent/heartbeat.go b/pkg/agent/heartbeat.go index 3d9b2ef..dae8ebf 100644 --- a/pkg/agent/heartbeat.go +++ b/pkg/agent/heartbeat.go @@ -1,6 +1,7 @@ package agent import ( + "context" "sync" "time" ) @@ -37,7 +38,7 @@ type Heartbeat interface { // interval - duration of interval // if service doesnt send heartbeat during interval it is marked offline. -func NewHeartbeat(name, svcType string, interval time.Duration) Heartbeat { +func NewHeartbeat(ctx context.Context, name, svcType string, interval time.Duration) Heartbeat { ticker := time.NewTicker(interval) s := svc{ info: Info{ @@ -49,13 +50,14 @@ func NewHeartbeat(name, svcType string, interval time.Duration) Heartbeat { ticker: ticker, interval: interval, } - s.listen() + go s.listen(ctx) return &s } -func (s *svc) listen() { - go func() { - for range s.ticker.C { +func (s *svc) listen(ctx context.Context) { + for { + select { + case <-s.ticker.C: // TODO - we can disable ticker when the status gets OFFLINE // and on the next heartbeat enable it again. s.mu.Lock() @@ -63,8 +65,10 @@ func (s *svc) listen() { s.info.Status = offline } s.mu.Unlock() + case <-ctx.Done(): + return } - }() + } } func (s *svc) Update() { diff --git a/pkg/agent/heartbeat_test.go b/pkg/agent/heartbeat_test.go index 5b603fa..6847635 100644 --- a/pkg/agent/heartbeat_test.go +++ b/pkg/agent/heartbeat_test.go @@ -1,6 +1,7 @@ package agent import ( + "context" "testing" "time" ) @@ -12,7 +13,8 @@ const ( ) func TestNewHeartbeat(t *testing.T) { - heartbeat := NewHeartbeat(name, serviceType, interval) + ctx, cancel := context.WithCancel(context.Background()) + heartbeat := NewHeartbeat(ctx, name, serviceType, interval) // Check initial status and info info := heartbeat.Info() @@ -25,10 +27,14 @@ func TestNewHeartbeat(t *testing.T) { if info.Status != online { t.Errorf("Expected initial status to be %s, but got %s", online, info.Status) } + t.Cleanup(func() { + cancel() + }) } func TestHeartbeat_Update(t *testing.T) { - heartbeat := NewHeartbeat(name, serviceType, interval) + ctx, cancel := context.WithCancel(context.Background()) + heartbeat := NewHeartbeat(ctx, name, serviceType, interval) // Sleep for more than the interval to simulate an update time.Sleep(3 * time.Second) @@ -40,10 +46,14 @@ func TestHeartbeat_Update(t *testing.T) { if info.Status != online { t.Errorf("Expected status to be %s, but got %s", online, info.Status) } + t.Cleanup(func() { + cancel() + }) } func TestHeartbeat_StatusOffline(t *testing.T) { - heartbeat := NewHeartbeat(name, serviceType, interval) + ctx, cancel := context.WithCancel(context.Background()) + heartbeat := NewHeartbeat(ctx, name, serviceType, interval) // Sleep for more than two intervals to simulate offline status time.Sleep(5 * time.Second) @@ -53,4 +63,7 @@ func TestHeartbeat_StatusOffline(t *testing.T) { if info.Status != offline { t.Errorf("Expected status to be %s, but got %s", offline, info.Status) } + t.Cleanup(func() { + cancel() + }) } diff --git a/pkg/agent/service.go b/pkg/agent/service.go index 4f43f64..b65ecd2 100644 --- a/pkg/agent/service.go +++ b/pkg/agent/service.go @@ -135,7 +135,7 @@ func (ag *agent) handle(ctx context.Context, pub messaging.Publisher, logger log // if there is multiple instances of the same service // we will have to add another distinction. if _, ok := ag.svcs[svcname]; !ok { - svc := NewHeartbeat(svcname, svctype, cfg.Interval) + svc := NewHeartbeat(ctx, svcname, svctype, cfg.Interval) ag.svcs[svcname] = svc ag.logger.Info(fmt.Sprintf("Services '%s-%s' registered", svcname, svctype)) } From 5173159c9a5f18ea5e2ae900d45b14585649a362 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 16:41:23 +0300 Subject: [PATCH 06/26] Fix race condition in Heartbeat.Info() method The `Info()` method in the `Heartbeat` interface was not thread-safe, which could lead to a race condition when accessing the `info` field. To fix this issue, a mutex was added to protect the access to `info` and ensure thread safety. The `Info()` method now locks the mutex before accessing `info` and unlocks it before returning the value. Additionally, a new `Close()` method was added to stop the heartbeat ticker when it is no longer needed. Signed-off-by: SammyOina --- pkg/agent/heartbeat.go | 10 +++++++++- pkg/agent/heartbeat_test.go | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/agent/heartbeat.go b/pkg/agent/heartbeat.go index dae8ebf..0bf3eae 100644 --- a/pkg/agent/heartbeat.go +++ b/pkg/agent/heartbeat.go @@ -34,6 +34,7 @@ type Info struct { type Heartbeat interface { Update() Info() Info + Close() } // interval - duration of interval @@ -79,5 +80,12 @@ func (s *svc) Update() { } func (s *svc) Info() Info { - return s.info + s.mu.Lock() + defer s.mu.Unlock() + info := s.info + return info +} + +func (s *svc) Close() { + s.ticker.Stop() } diff --git a/pkg/agent/heartbeat_test.go b/pkg/agent/heartbeat_test.go index 6847635..f913315 100644 --- a/pkg/agent/heartbeat_test.go +++ b/pkg/agent/heartbeat_test.go @@ -29,6 +29,7 @@ func TestNewHeartbeat(t *testing.T) { } t.Cleanup(func() { cancel() + heartbeat.Close() }) } @@ -48,6 +49,7 @@ func TestHeartbeat_Update(t *testing.T) { } t.Cleanup(func() { cancel() + heartbeat.Close() }) } @@ -65,5 +67,6 @@ func TestHeartbeat_StatusOffline(t *testing.T) { } t.Cleanup(func() { cancel() + heartbeat.Close() }) } From 54eb0c47894f9733dc4e85a97a37d1c2775621fc Mon Sep 17 00:00:00 2001 From: SammyOina Date: Fri, 22 Sep 2023 18:19:39 +0300 Subject: [PATCH 07/26] Fix error handling in agent/config.go The commit fixes error handling in agent/config.go by adding more specific error variables and updating the error messages. This improves the clarity and readability of the codebase. Signed-off-by: SammyOina --- pkg/agent/config.go | 16 +++--- pkg/agent/config_test.go | 103 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 pkg/agent/config_test.go diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 0f61aa2..cbb59eb 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -6,7 +6,6 @@ package agent import ( "crypto/tls" "encoding/json" - "fmt" "os" "time" @@ -14,7 +13,12 @@ import ( "github.com/pelletier/go-toml" ) -var ErrWritingToml = errors.New("error writing to toml file") +var ( + ErrWritingToml = errors.New("error writing to toml file") + errReadingFile = errors.New("error reading config file") + errUnmarshalToml = errors.New("error unmarshaling toml") + errMarshalToml = errors.New("error marshaling toml") +) type ServerConfig struct { Port string `toml:"port" json:"port"` @@ -88,7 +92,7 @@ func NewConfig(sc ServerConfig, cc ChanConfig, ec EdgexConfig, lc LogConfig, mc func SaveConfig(c Config) error { b, err := toml.Marshal(c) if err != nil { - return errors.New(fmt.Sprintf("Error reading config file: %s", err)) + return errors.Wrap(errMarshalToml, err) } if err := os.WriteFile(c.File, b, 0644); err != nil { return errors.Wrap(ErrWritingToml, err) @@ -96,16 +100,16 @@ func SaveConfig(c Config) error { return nil } -// Read - retrieve config from a file. +// ReadConfig - retrieve config from a file. func ReadConfig(file string) (Config, error) { data, err := os.ReadFile(file) c := Config{} if err != nil { - return c, errors.New(fmt.Sprintf("Error reading config file: %s", err)) + return Config{}, errors.Wrap(errReadingFile, err) } if err := toml.Unmarshal(data, &c); err != nil { - return Config{}, errors.New(fmt.Sprintf("Error unmarshaling toml: %s", err)) + return Config{}, errors.Wrap(errUnmarshalToml, err) } return c, nil } diff --git a/pkg/agent/config_test.go b/pkg/agent/config_test.go new file mode 100644 index 0000000..e461f80 --- /dev/null +++ b/pkg/agent/config_test.go @@ -0,0 +1,103 @@ +package agent + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/mainflux/mainflux/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestReadConfig(t *testing.T) { + // Create a temporary config file for testing. + tempFile, err := os.CreateTemp("", "config.toml") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tempFile.Name()) + tempFile2, err := os.CreateTemp("", "invalid.toml") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tempFile2.Name()) + + sampleConfig := ` + File = "config.toml" + + [channels] + control = "" + data = "" + + [edgex] + url = "http://localhost:48090/api/v1/" + + [heartbeat] + interval = "10s" + + [log] + level = "info" + + [mqtt] + ca_cert = "" + ca_path = "ca.crt" + cert_path = "thing.cert" + client_cert = "" + client_key = "" + mtls = false + password = "" + priv_key_path = "thing.key" + qos = 0 + retain = false + skip_tls_ver = true + url = "localhost:1883" + username = "" + + [server] + nats_url = "nats://127.0.0.1:4222" + port = "9999" + + [terminal] + session_timeout = "1m0s" +` + + if _, writeErr := tempFile.WriteString(sampleConfig); writeErr != nil { + t.Fatalf("Failed to write to temporary file: %v", writeErr) + } + tempFile.Close() + + if _, writeErr := tempFile2.WriteString(strings.ReplaceAll(sampleConfig, "[", "")); writeErr != nil { + t.Fatalf("Failed to write to temporary file: %v", writeErr) + } + tempFile2.Close() + + tests := []struct { + name string + fileName string + expectedErr error + }{ + { + name: "failed to read file", + fileName: "invalidFile.toml", + expectedErr: errReadingFile, + }, + { + name: "invalid toml", + fileName: tempFile2.Name(), + expectedErr: errUnmarshalToml, + }, + { + name: "successful read", + fileName: tempFile.Name(), + expectedErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := ReadConfig(test.fileName) + assert.True(t, errors.Contains(err, test.expectedErr), fmt.Sprintf("expected %v got %v", test.expectedErr, err)) + }) + } +} From bc627139009bc957f34b3cec6f6a66533269b00d Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 10:46:25 +0300 Subject: [PATCH 08/26] test ci Signed-off-by: SammyOina --- .github/workflows/test.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..16f6e69 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Build and Publish Docker Image +on: + push: + branches: + - tests # Change this to your main branch name if different + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.21 # Set the Go version you want to use + + - name: Build Docker Images + run: | + make dockers + + - name: Docker Login + run: | + echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + env: + DOCKER_USERNAME: ${{ github.actor }} + DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + + - name: Push Docker Images + run: | + make latest From 37454e28fa531b237645388b66088a826a83e765 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 10:49:15 +0300 Subject: [PATCH 09/26] test push Signed-off-by: SammyOina --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a33efc5..60754a5 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ define make_docker --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg TIME=$(TIME) \ - --tag=mainflux/$(svc) \ + --tag=sammyoina/$(svc) \ -f docker/Dockerfile . endef @@ -82,7 +82,7 @@ dockers_dev: $(DOCKERS_DEV) define docker_push for svc in $(SERVICES); do \ - docker push mainflux/$$svc:$(1); \ + docker push sammyoina/$$svc:$(1); \ done endef From 3e128d3cd06d9599333dd9e131164e10dfb8c4b2 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 10:55:37 +0300 Subject: [PATCH 10/26] test 22 Signed-off-by: SammyOina --- .github/workflows/build22.yml | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/build22.yml diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml new file mode 100644 index 0000000..2f1247a --- /dev/null +++ b/.github/workflows/build22.yml @@ -0,0 +1,43 @@ +name: Create and publish a Docker image + +on: + push: + branches: ['main'] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + build-args: SVC=apigate,VERSION=latest + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} From 389cf39c3a93676df1bd34f0fc168ef36f6874aa Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 11:02:35 +0300 Subject: [PATCH 11/26] test target Signed-off-by: SammyOina --- .github/workflows/build22.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml index 2f1247a..d71e2e6 100644 --- a/.github/workflows/build22.yml +++ b/.github/workflows/build22.yml @@ -2,7 +2,7 @@ name: Create and publish a Docker image on: push: - branches: ['main'] + branches: ['tests'] env: REGISTRY: ghcr.io From cb91759d3225fa10ca1e26959a60bd41a42b7862 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 11:05:23 +0300 Subject: [PATCH 12/26] switch context Signed-off-by: SammyOina --- .github/workflows/build22.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml index d71e2e6..49276a4 100644 --- a/.github/workflows/build22.yml +++ b/.github/workflows/build22.yml @@ -36,7 +36,7 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: - context: . + context: ./docker push: true build-args: SVC=apigate,VERSION=latest tags: ${{ steps.meta.outputs.tags }} From 9319d04d71bb72d03dd32f54a928a8f7fe26cbbd Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 11:21:51 +0300 Subject: [PATCH 13/26] switch context Signed-off-by: SammyOina --- .github/workflows/build22.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml index 49276a4..67a1784 100644 --- a/.github/workflows/build22.yml +++ b/.github/workflows/build22.yml @@ -36,7 +36,8 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: - context: ./docker + context: . + file: ./docker/Dockerfile push: true build-args: SVC=apigate,VERSION=latest tags: ${{ steps.meta.outputs.tags }} From 8ed7430c27a8884001ffc1baef98272e3463bd46 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 11:26:27 +0300 Subject: [PATCH 14/26] make agent Signed-off-by: SammyOina --- .github/workflows/build22.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml index 67a1784..edb375d 100644 --- a/.github/workflows/build22.yml +++ b/.github/workflows/build22.yml @@ -39,6 +39,6 @@ jobs: context: . file: ./docker/Dockerfile push: true - build-args: SVC=apigate,VERSION=latest + build-args: SVC=agent,VERSION=latest tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 23fca334e5056cb6d8ef75c1eda5e109a57aec3e Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 11:42:02 +0300 Subject: [PATCH 15/26] debug build Signed-off-by: SammyOina --- docker/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 94198e0..a260011 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,8 +10,9 @@ WORKDIR /go/src/github.com/mainflux/agent COPY . . RUN apk update \ && apk add make\ - && make $SVC \ - && mv build/mainflux-$SVC /exe + && make $SVC +RUN ls build +RUN mv build/mainflux-$SVC /exe FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt From 3577f0ea5577e024908f88cb28ab01c1fafd05bf Mon Sep 17 00:00:00 2001 From: SammyOina Date: Tue, 26 Sep 2023 11:54:40 +0300 Subject: [PATCH 16/26] build args Signed-off-by: SammyOina --- .github/workflows/build22.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml index edb375d..aa46ef1 100644 --- a/.github/workflows/build22.yml +++ b/.github/workflows/build22.yml @@ -39,6 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - build-args: SVC=agent,VERSION=latest + build-args: | + SVC=agent tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 37ccb0497d255a6c4868cb343d9b5e38cef4ce91 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 11:43:45 +0300 Subject: [PATCH 17/26] Add terminal tests Refactor build workflow The build22.yml file has been deleted. This commit removes the build22.yml file, which was used to create and publish a Docker image. The file is no longer needed as it is no longer being used in the project. Signed-off-by: SammyOina --- .github/workflows/build22.yml | 45 --------------------- pkg/terminal/terminal.go | 5 +-- pkg/terminal/terminal_test.go | 73 +++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 48 deletions(-) delete mode 100644 .github/workflows/build22.yml create mode 100644 pkg/terminal/terminal_test.go diff --git a/.github/workflows/build22.yml b/.github/workflows/build22.yml deleted file mode 100644 index aa46ef1..0000000 --- a/.github/workflows/build22.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Create and publish a Docker image - -on: - push: - branches: ['tests'] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - file: ./docker/Dockerfile - push: true - build-args: | - SVC=agent - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go index 6987a3e..0a04207 100644 --- a/pkg/terminal/terminal.go +++ b/pkg/terminal/terminal.go @@ -13,7 +13,6 @@ import ( "github.com/mainflux/agent/pkg/encoder" "github.com/mainflux/mainflux/logger" - "github.com/mainflux/mainflux/pkg/errors" ) const ( @@ -54,7 +53,7 @@ func NewSession(uuid string, timeout time.Duration, publish func(channel, payloa c := exec.Command("bash") ptmx, err := pty.Start(c) if err != nil { - return t, errors.New(err.Error()) + return t, err } t.ptmx = ptmx @@ -120,7 +119,7 @@ func (t *term) Send(p []byte) error { nr, err := io.Copy(t.ptmx, in) t.logger.Debug(fmt.Sprintf("Written to ptmx: %d", nr)) if err != nil { - return errors.New(err.Error()) + return err } return nil } diff --git a/pkg/terminal/terminal_test.go b/pkg/terminal/terminal_test.go new file mode 100644 index 0000000..977a7a2 --- /dev/null +++ b/pkg/terminal/terminal_test.go @@ -0,0 +1,73 @@ +package terminal + +import ( + "testing" + "time" + + "github.com/mainflux/mainflux/logger" + "github.com/stretchr/testify/assert" +) + +// MockPublish is a mock function for the publish function used in NewSession. +func MockPublish(channel, payload string) error { + return nil +} + +func TestSessionWrite(t *testing.T) { + publish := MockPublish + + uuid := "test-uuid" + timeout := 5 * time.Second + + session, err := NewSession(uuid, timeout, publish, logger.NewMock()) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + // Simulate writing data to the session + data := []byte("test data") + n, err := session.Write(data) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + assert.Equal(t, len(data), n) +} + +func TestSessionSend(t *testing.T) { + publish := MockPublish + + uuid := "test-uuid" + timeout := 5 * time.Second + + session, err := NewSession(uuid, timeout, publish, logger.NewMock()) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + // Simulate sending data to the session + data := []byte("test data") + + if err = session.Send(data); err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } +} + +func TestSessionIsDone(t *testing.T) { + publish := MockPublish + + uuid := "test-uuid" + timeout := 5 * time.Second + + session, err := NewSession(uuid, timeout, publish, logger.NewMock()) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + + // Wait for the "done" channel to be closed or for a timeout, and perform assertions accordingly. + select { + case <-session.IsDone(): + // Session is done as expected. + case <-time.After(10 * time.Second): + t.Fatalf("Expected session to be done, but it is still running.") + } +} From 2dc7996ae07a68f282ce383f93e412889b17e3d6 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 12:23:18 +0300 Subject: [PATCH 18/26] Add test for PushOperation function in edgex package This commit adds a new test for the PushOperation function in the edgex package. The test creates a mock HTTP server and checks the HTTP request method, URL, and request body. The test ensures that the function correctly handles the POST request with the expected URL and request body. Signed-off-by: SammyOina --- pkg/edgex/client_test.go | 158 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 pkg/edgex/client_test.go diff --git a/pkg/edgex/client_test.go b/pkg/edgex/client_test.go new file mode 100644 index 0000000..60788e3 --- /dev/null +++ b/pkg/edgex/client_test.go @@ -0,0 +1,158 @@ +package edgex + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mainflux/mainflux/logger" +) + +func TestPushOperation(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check the HTTP request + if r.Method != http.MethodPost { + t.Errorf("Expected POST request, got %s", r.Method) + } + + // Check the URL + expectedURL := "/operation" + if r.URL.String() != expectedURL { + t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) + } + + // Check the request body + expectedBody := `{"action":"start","services":["service1","service2"]}` + bodyBytes, _ := io.ReadAll(r.Body) + if string(bodyBytes) != expectedBody { + t.Errorf("Expected request body %s, got %s", expectedBody, string(bodyBytes)) + } + + // Respond with a dummy response + w.WriteHeader(http.StatusOK) + w.Write([]byte("Response")) + })) + defer server.Close() + + // Create an edgexClient with the mock server URL + client := NewClient(server.URL+"/", logger.NewMock()) + + // Test PushOperation + response, err := client.PushOperation([]string{"start", "service1", "service2"}) + if err != nil { + t.Errorf("Error calling PushOperation: %v", err) + } + + expectedResponse := "Response" + if response != expectedResponse { + t.Errorf("Expected response %s, got %s", expectedResponse, response) + } +} + +func TestFetchConfig(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check the HTTP request + if r.Method != http.MethodGet { + t.Errorf("Expected GET request, got %s", r.Method) + } + + // Check the URL + expectedURL := "/config/start,service1,service2" + if r.URL.String() != expectedURL { + t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) + } + + // Respond with a dummy response + w.WriteHeader(http.StatusOK) + w.Write([]byte("Response")) + })) + defer server.Close() + + // Create an edgexClient with the mock server URL + client := NewClient(server.URL+"/", logger.NewMock()) + + // Test FetchConfig + response, err := client.FetchConfig([]string{"start", "service1", "service2"}) + if err != nil { + t.Errorf("Error calling FetchConfig: %v", err) + } + + expectedResponse := "Response" + if response != expectedResponse { + t.Errorf("Expected response %s, got %s", expectedResponse, response) + } +} + +func TestFetchMetrics(t *testing.T) { + expectedResponse := "Response" + + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check the HTTP request + if r.Method != http.MethodGet { + t.Errorf("Expected GET request, got %s", r.Method) + } + + // Check the URL + expectedURL := "/metrics/start,service1,service2" + if r.URL.String() != expectedURL { + t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) + } + + // Respond with a dummy response + w.WriteHeader(http.StatusOK) + w.Write([]byte(expectedResponse)) + })) + defer server.Close() + + // Create an edgexClient with the mock server URL + client := NewClient(server.URL+"/", logger.NewMock()) + + // Test FetchMetrics + response, err := client.FetchMetrics([]string{"start", "service1", "service2"}) + if err != nil { + t.Errorf("Error calling FetchMetrics: %v", err) + } + + if response != expectedResponse { + t.Errorf("Expected response %s, got %s", expectedResponse, response) + } +} + +func TestPing(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check the HTTP request + if r.Method != http.MethodGet { + t.Errorf("Expected GET request, got %s", r.Method) + } + + // Check the URL + expectedURL := "/ping" + if r.URL.String() != expectedURL { + t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) + } + + // Respond with a dummy response + w.WriteHeader(http.StatusOK) + w.Write([]byte("Response")) + })) + defer server.Close() + + // Create an edgexClient with the mock server URL + client := NewClient(server.URL+"/", logger.NewMock()) + + // Test Ping + response, err := client.Ping() + if err != nil { + t.Errorf("Error calling Ping: %v", err) + } + + expectedResponse := "Response" + if response != expectedResponse { + t.Errorf("Expected response %s, got %s", expectedResponse, response) + } +} From 400dbb1981af6cc62c2f46081e05f50d7ddf840a Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 12:50:28 +0300 Subject: [PATCH 19/26] Add tests for the conn package This commit adds tests for the conn package. The tests cover various scenarios and ensure the functionality of the package is working as expected. Signed-off-by: SammyOina --- pkg/conn/conn_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 pkg/conn/conn_test.go diff --git a/pkg/conn/conn_test.go b/pkg/conn/conn_test.go new file mode 100644 index 0000000..f9296f3 --- /dev/null +++ b/pkg/conn/conn_test.go @@ -0,0 +1,136 @@ +package conn + +import ( + "context" + "testing" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/mainflux/agent/pkg/agent" + "github.com/mainflux/mainflux/logger" + "github.com/mainflux/mainflux/pkg/messaging" + "github.com/stretchr/testify/assert" +) + +// Mocks for testing +type mockService struct{} + +func (m *mockService) Config() agent.Config { return agent.Config{} } +func (m *mockService) Services() []agent.Info { return []agent.Info{} } +func (m *mockService) Publish(string, string) error { return nil } +func (m *mockService) AddConfig(agent.Config) error { return nil } +func (m *mockService) Control(uuid, command string) error { return nil } +func (m *mockService) Execute(uuid, command string) (string, error) { return "", nil } +func (m *mockService) ServiceConfig(ctx context.Context, uuid, command string) error { + return nil +} +func (m *mockService) Terminal(uuid, command string) error { return nil } + +type mockMQTTClient struct { + subscribeErr error + waitErr error +} + +// AddRoute implements mqtt.Client. +func (*mockMQTTClient) AddRoute(topic string, callback mqtt.MessageHandler) { + panic("unimplemented") +} + +// Connect implements mqtt.Client. +func (*mockMQTTClient) Connect() mqtt.Token { + panic("unimplemented") +} + +// Disconnect implements mqtt.Client. +func (*mockMQTTClient) Disconnect(quiesce uint) { + panic("unimplemented") +} + +// IsConnected implements mqtt.Client. +func (*mockMQTTClient) IsConnected() bool { + panic("unimplemented") +} + +// IsConnectionOpen implements mqtt.Client. +func (*mockMQTTClient) IsConnectionOpen() bool { + panic("unimplemented") +} + +// OptionsReader implements mqtt.Client. +func (*mockMQTTClient) OptionsReader() mqtt.ClientOptionsReader { + panic("unimplemented") +} + +// Publish implements mqtt.Client. +func (*mockMQTTClient) Publish(topic string, qos byte, retained bool, payload interface{}) mqtt.Token { + panic("unimplemented") +} + +// SubscribeMultiple implements mqtt.Client. +func (*mockMQTTClient) SubscribeMultiple(filters map[string]byte, callback mqtt.MessageHandler) mqtt.Token { + panic("unimplemented") +} + +// Unsubscribe implements mqtt.Client. +func (*mockMQTTClient) Unsubscribe(topics ...string) mqtt.Token { + panic("unimplemented") +} + +func (m *mockMQTTClient) Subscribe(topic string, qos byte, callback mqtt.MessageHandler) mqtt.Token { + return &mockToken{err: m.subscribeErr} +} + +func (m *mockMQTTClient) Wait() bool { + return m.waitErr == nil +} + +type mockToken struct { + err error +} + +func (m *mockToken) Wait() bool { return true } +func (m *mockToken) WaitTimeout(time.Duration) bool { return true } +func (m *mockToken) Error() error { return m.err } +func (m *mockToken) Done() <-chan struct{} { + x := make(chan struct{}) + return x +} + +type mockMessageBroker struct { + publishErr error +} + +// Close implements messaging.PubSub. +func (*mockMessageBroker) Close() error { + panic("unimplemented") +} + +// Subscribe implements messaging.PubSub. +func (*mockMessageBroker) Subscribe(ctx context.Context, id string, topic string, handler messaging.MessageHandler) error { + panic("unimplemented") +} + +// Unsubscribe implements messaging.PubSub. +func (*mockMessageBroker) Unsubscribe(ctx context.Context, id string, topic string) error { + panic("unimplemented") +} + +func (m *mockMessageBroker) Publish(ctx context.Context, topic string, msg *messaging.Message) error { + return m.publishErr +} + +func TestBroker_Subscribe(t *testing.T) { + svc := &mockService{} + client := &mockMQTTClient{} + chann := "test" + messBroker := &mockMessageBroker{} + + broker := NewBroker(svc, client, chann, messBroker, logger.NewMock()) + + assert.NotNil(t, broker) + + ctx := context.Background() + err := broker.Subscribe(ctx) + + assert.NoError(t, err) +} From 4a4b4ec2d05c155fd0f93cc7ec56a9f16de0d660 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 13:04:49 +0300 Subject: [PATCH 20/26] Fix bug in client_test.go The commit fixes a bug in the client_test.go file where the expected response was not being properly set. The bug caused the test to fail. The fix ensures that the expected response is correctly set and the test passes as expected. Signed-off-by: SammyOina --- pkg/edgex/client_test.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pkg/edgex/client_test.go b/pkg/edgex/client_test.go index 60788e3..9bb0cc6 100644 --- a/pkg/edgex/client_test.go +++ b/pkg/edgex/client_test.go @@ -10,6 +10,7 @@ import ( ) func TestPushOperation(t *testing.T) { + expectedResponse := "Response" // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check the HTTP request @@ -32,7 +33,9 @@ func TestPushOperation(t *testing.T) { // Respond with a dummy response w.WriteHeader(http.StatusOK) - w.Write([]byte("Response")) + if _, err := w.Write([]byte(expectedResponse)); err != nil { + t.Errorf("error writing response %v", err) + } })) defer server.Close() @@ -45,13 +48,14 @@ func TestPushOperation(t *testing.T) { t.Errorf("Error calling PushOperation: %v", err) } - expectedResponse := "Response" if response != expectedResponse { t.Errorf("Expected response %s, got %s", expectedResponse, response) } } func TestFetchConfig(t *testing.T) { + expectedResponse := "Response" + // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check the HTTP request @@ -67,7 +71,9 @@ func TestFetchConfig(t *testing.T) { // Respond with a dummy response w.WriteHeader(http.StatusOK) - w.Write([]byte("Response")) + if _, err := w.Write([]byte(expectedResponse)); err != nil { + t.Errorf("error writing response %v", err) + } })) defer server.Close() @@ -80,7 +86,6 @@ func TestFetchConfig(t *testing.T) { t.Errorf("Error calling FetchConfig: %v", err) } - expectedResponse := "Response" if response != expectedResponse { t.Errorf("Expected response %s, got %s", expectedResponse, response) } @@ -104,7 +109,9 @@ func TestFetchMetrics(t *testing.T) { // Respond with a dummy response w.WriteHeader(http.StatusOK) - w.Write([]byte(expectedResponse)) + if _, err := w.Write([]byte(expectedResponse)); err != nil { + t.Errorf("error writing response %v", err) + } })) defer server.Close() @@ -123,6 +130,7 @@ func TestFetchMetrics(t *testing.T) { } func TestPing(t *testing.T) { + expectedResponse := "Response" // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check the HTTP request @@ -138,7 +146,9 @@ func TestPing(t *testing.T) { // Respond with a dummy response w.WriteHeader(http.StatusOK) - w.Write([]byte("Response")) + if _, err := w.Write([]byte(expectedResponse)); err != nil { + t.Errorf("error writing response %v", err) + } })) defer server.Close() @@ -151,7 +161,6 @@ func TestPing(t *testing.T) { t.Errorf("Error calling Ping: %v", err) } - expectedResponse := "Response" if response != expectedResponse { t.Errorf("Expected response %s, got %s", expectedResponse, response) } From 09b6fc1882022080c71b4305a9c07f6a1a253c2c Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 13:28:50 +0300 Subject: [PATCH 21/26] Fix error handling in main.go and endpoints_test.go The commit fixes error handling in main.go and endpoints_test.go. It ensures that the server is properly shutdown and logs any errors that occur during the shutdown process. Signed-off-by: SammyOina --- cmd/main.go | 3 ++- pkg/agent/api/endpoints_test.go | 3 +++ pkg/agent/api/logging.go | 13 +++++++++++++ pkg/agent/api/metrics.go | 13 +++++++++++-- pkg/agent/service.go | 11 +++++++++++ pkg/conn/conn_test.go | 3 ++- pkg/edgex/client_test.go | 6 ++---- pkg/terminal/terminal_test.go | 14 +++++--------- 8 files changed, 49 insertions(+), 17 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 7f26438..2ec3fbb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -129,6 +129,7 @@ func main() { logger.Error(fmt.Sprintf("Error in agent service: %s", err)) return } + defer svc.Close() svc = api.LoggingMiddleware(svc, logger) svc = api.MetricsMiddleware( @@ -402,7 +403,7 @@ func StopSignalHandler(ctx context.Context, cancel context.CancelFunc, logger lo shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 5*time.Second) defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { - return fmt.Errorf("Failed to shutdown %s server: %v", svcName, err) + return fmt.Errorf("failed to shutdown %s server: %v", svcName, err) } return fmt.Errorf("%s service shutdown by signal: %s", svcName, sig) case <-ctx.Done(): diff --git a/pkg/agent/api/endpoints_test.go b/pkg/agent/api/endpoints_test.go index 13a547e..7c94fef 100644 --- a/pkg/agent/api/endpoints_test.go +++ b/pkg/agent/api/endpoints_test.go @@ -111,6 +111,9 @@ func TestPublish(t *testing.T) { {"publish data", data, http.StatusOK}, {"publish data with invalid data", "}", http.StatusInternalServerError}, } + t.Cleanup(func() { + assert.Nil(t, svc.Close()) + }) for _, tc := range cases { req := testRequest{ diff --git a/pkg/agent/api/logging.go b/pkg/agent/api/logging.go index 5d98931..2e93f47 100644 --- a/pkg/agent/api/logging.go +++ b/pkg/agent/api/logging.go @@ -119,3 +119,16 @@ func (lm loggingMiddleware) Terminal(uuid, cmdStr string) (err error) { return lm.svc.Terminal(uuid, cmdStr) } + +func (lm loggingMiddleware) Close() (err error) { + defer func(begin time.Time) { + message := fmt.Sprintf("Method close took %s to complete", time.Since(begin)) + if err != nil { + lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err)) + return + } + lm.logger.Info(fmt.Sprintf("%s without errors.", message)) + }(time.Now()) + + return lm.svc.Close() +} diff --git a/pkg/agent/api/metrics.go b/pkg/agent/api/metrics.go index 5e58742..c22d4b2 100644 --- a/pkg/agent/api/metrics.go +++ b/pkg/agent/api/metrics.go @@ -96,9 +96,18 @@ func (ms *metricsMiddleware) Publish(topic, payload string) error { func (ms *metricsMiddleware) Terminal(topic, payload string) error { defer func(begin time.Time) { - ms.counter.With("method", "publish").Add(1) - ms.latency.With("method", "publish").Observe(time.Since(begin).Seconds()) + ms.counter.With("method", "terminal").Add(1) + ms.latency.With("method", "terminal").Observe(time.Since(begin).Seconds()) }(time.Now()) return ms.svc.Terminal(topic, payload) } + +func (ms *metricsMiddleware) Close() error { + defer func(begin time.Time) { + ms.counter.With("method", "close").Add(1) + ms.latency.With("method", "close").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return ms.svc.Close() +} diff --git a/pkg/agent/service.go b/pkg/agent/service.go index b65ecd2..8506c6b 100644 --- a/pkg/agent/service.go +++ b/pkg/agent/service.go @@ -107,6 +107,9 @@ type Service interface { // Publish message. Publish(string, string) error + + // Closes all connections. + Close() error } var _ Service = (*agent)(nil) @@ -121,6 +124,14 @@ type agent struct { terminals map[string]terminal.Session } +func (ag *agent) Close() error { + ag.mqttClient.Disconnect(1) + for _, svc := range ag.svcs { + svc.Close() + } + return ag.broker.Close() +} + func (ag *agent) handle(ctx context.Context, pub messaging.Publisher, logger log.Logger, cfg HeartbeatConfig) handleFunc { return func(msg *messaging.Message) error { sub := msg.Channel diff --git a/pkg/conn/conn_test.go b/pkg/conn/conn_test.go index f9296f3..8ac2545 100644 --- a/pkg/conn/conn_test.go +++ b/pkg/conn/conn_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" ) -// Mocks for testing +// Mocks for testing. type mockService struct{} func (m *mockService) Config() agent.Config { return agent.Config{} } @@ -24,6 +24,7 @@ func (m *mockService) Execute(uuid, command string) (string, error) { return "", func (m *mockService) ServiceConfig(ctx context.Context, uuid, command string) error { return nil } +func (m *mockService) Close() error { return nil } func (m *mockService) Terminal(uuid, command string) error { return nil } type mockMQTTClient struct { diff --git a/pkg/edgex/client_test.go b/pkg/edgex/client_test.go index 9bb0cc6..89bc518 100644 --- a/pkg/edgex/client_test.go +++ b/pkg/edgex/client_test.go @@ -9,8 +9,9 @@ import ( "github.com/mainflux/mainflux/logger" ) +const expectedResponse = "Response" + func TestPushOperation(t *testing.T) { - expectedResponse := "Response" // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check the HTTP request @@ -54,7 +55,6 @@ func TestPushOperation(t *testing.T) { } func TestFetchConfig(t *testing.T) { - expectedResponse := "Response" // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -92,7 +92,6 @@ func TestFetchConfig(t *testing.T) { } func TestFetchMetrics(t *testing.T) { - expectedResponse := "Response" // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -130,7 +129,6 @@ func TestFetchMetrics(t *testing.T) { } func TestPing(t *testing.T) { - expectedResponse := "Response" // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check the HTTP request diff --git a/pkg/terminal/terminal_test.go b/pkg/terminal/terminal_test.go index 977a7a2..6c06e51 100644 --- a/pkg/terminal/terminal_test.go +++ b/pkg/terminal/terminal_test.go @@ -8,6 +8,11 @@ import ( "github.com/stretchr/testify/assert" ) +const ( + uuid = "test-uuid" + timeout = 5 * time.Second +) + // MockPublish is a mock function for the publish function used in NewSession. func MockPublish(channel, payload string) error { return nil @@ -16,9 +21,6 @@ func MockPublish(channel, payload string) error { func TestSessionWrite(t *testing.T) { publish := MockPublish - uuid := "test-uuid" - timeout := 5 * time.Second - session, err := NewSession(uuid, timeout, publish, logger.NewMock()) if err != nil { t.Fatalf("Expected no error, but got: %v", err) @@ -36,9 +38,6 @@ func TestSessionWrite(t *testing.T) { func TestSessionSend(t *testing.T) { publish := MockPublish - uuid := "test-uuid" - timeout := 5 * time.Second - session, err := NewSession(uuid, timeout, publish, logger.NewMock()) if err != nil { t.Fatalf("Expected no error, but got: %v", err) @@ -55,9 +54,6 @@ func TestSessionSend(t *testing.T) { func TestSessionIsDone(t *testing.T) { publish := MockPublish - uuid := "test-uuid" - timeout := 5 * time.Second - session, err := NewSession(uuid, timeout, publish, logger.NewMock()) if err != nil { t.Fatalf("Expected no error, but got: %v", err) From 397d40e6ebab56edf6d59bf1770ab7822344f995 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 14:37:55 +0300 Subject: [PATCH 22/26] remove test.yml Signed-off-by: SammyOina --- .github/workflows/test.yml | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 16f6e69..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Build and Publish Docker Image -on: - push: - branches: - - tests # Change this to your main branch name if different - -jobs: - build-and-publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v2 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.21 # Set the Go version you want to use - - - name: Build Docker Images - run: | - make dockers - - - name: Docker Login - run: | - echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - env: - DOCKER_USERNAME: ${{ github.actor }} - DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - - - name: Push Docker Images - run: | - make latest From e667a834b57e0a036fe44c89e69dfad350b1f57c Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 14:45:53 +0300 Subject: [PATCH 23/26] Fix Docker image tag and file path in Makefile and Dockerfile The Docker image tag and file path in the Makefile and Dockerfile have been fixed to use the correct repository name "Mainflux" instead of "sammyoina". This ensures that the Docker images are tagged and pushed to the correct repository. Additionally, the file path in the Dockerfile has been updated to correctly move the built executable to the desired location. Signed-off-by: SammyOina --- Makefile | 4 ++-- docker/Dockerfile | 5 ++--- pkg/edgex/client_test.go | 27 --------------------------- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index 60754a5..5448bbb 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ define make_docker --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg TIME=$(TIME) \ - --tag=sammyoina/$(svc) \ + --tag=Mainflux/$(svc) \ -f docker/Dockerfile . endef @@ -82,7 +82,7 @@ dockers_dev: $(DOCKERS_DEV) define docker_push for svc in $(SERVICES); do \ - docker push sammyoina/$$svc:$(1); \ + docker push mainflux/$$svc:$(1); \ done endef diff --git a/docker/Dockerfile b/docker/Dockerfile index a260011..94198e0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,9 +10,8 @@ WORKDIR /go/src/github.com/mainflux/agent COPY . . RUN apk update \ && apk add make\ - && make $SVC -RUN ls build -RUN mv build/mainflux-$SVC /exe + && make $SVC \ + && mv build/mainflux-$SVC /exe FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt diff --git a/pkg/edgex/client_test.go b/pkg/edgex/client_test.go index 89bc518..5175042 100644 --- a/pkg/edgex/client_test.go +++ b/pkg/edgex/client_test.go @@ -12,27 +12,22 @@ import ( const expectedResponse = "Response" func TestPushOperation(t *testing.T) { - // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check the HTTP request if r.Method != http.MethodPost { t.Errorf("Expected POST request, got %s", r.Method) } - // Check the URL expectedURL := "/operation" if r.URL.String() != expectedURL { t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) } - // Check the request body expectedBody := `{"action":"start","services":["service1","service2"]}` bodyBytes, _ := io.ReadAll(r.Body) if string(bodyBytes) != expectedBody { t.Errorf("Expected request body %s, got %s", expectedBody, string(bodyBytes)) } - // Respond with a dummy response w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte(expectedResponse)); err != nil { t.Errorf("error writing response %v", err) @@ -40,10 +35,8 @@ func TestPushOperation(t *testing.T) { })) defer server.Close() - // Create an edgexClient with the mock server URL client := NewClient(server.URL+"/", logger.NewMock()) - // Test PushOperation response, err := client.PushOperation([]string{"start", "service1", "service2"}) if err != nil { t.Errorf("Error calling PushOperation: %v", err) @@ -55,21 +48,16 @@ func TestPushOperation(t *testing.T) { } func TestFetchConfig(t *testing.T) { - - // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check the HTTP request if r.Method != http.MethodGet { t.Errorf("Expected GET request, got %s", r.Method) } - // Check the URL expectedURL := "/config/start,service1,service2" if r.URL.String() != expectedURL { t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) } - // Respond with a dummy response w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte(expectedResponse)); err != nil { t.Errorf("error writing response %v", err) @@ -77,10 +65,8 @@ func TestFetchConfig(t *testing.T) { })) defer server.Close() - // Create an edgexClient with the mock server URL client := NewClient(server.URL+"/", logger.NewMock()) - // Test FetchConfig response, err := client.FetchConfig([]string{"start", "service1", "service2"}) if err != nil { t.Errorf("Error calling FetchConfig: %v", err) @@ -92,21 +78,16 @@ func TestFetchConfig(t *testing.T) { } func TestFetchMetrics(t *testing.T) { - - // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check the HTTP request if r.Method != http.MethodGet { t.Errorf("Expected GET request, got %s", r.Method) } - // Check the URL expectedURL := "/metrics/start,service1,service2" if r.URL.String() != expectedURL { t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) } - // Respond with a dummy response w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte(expectedResponse)); err != nil { t.Errorf("error writing response %v", err) @@ -114,10 +95,8 @@ func TestFetchMetrics(t *testing.T) { })) defer server.Close() - // Create an edgexClient with the mock server URL client := NewClient(server.URL+"/", logger.NewMock()) - // Test FetchMetrics response, err := client.FetchMetrics([]string{"start", "service1", "service2"}) if err != nil { t.Errorf("Error calling FetchMetrics: %v", err) @@ -129,20 +108,16 @@ func TestFetchMetrics(t *testing.T) { } func TestPing(t *testing.T) { - // Create a mock HTTP server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Check the HTTP request if r.Method != http.MethodGet { t.Errorf("Expected GET request, got %s", r.Method) } - // Check the URL expectedURL := "/ping" if r.URL.String() != expectedURL { t.Errorf("Expected URL %s, got %s", expectedURL, r.URL.String()) } - // Respond with a dummy response w.WriteHeader(http.StatusOK) if _, err := w.Write([]byte(expectedResponse)); err != nil { t.Errorf("error writing response %v", err) @@ -150,10 +125,8 @@ func TestPing(t *testing.T) { })) defer server.Close() - // Create an edgexClient with the mock server URL client := NewClient(server.URL+"/", logger.NewMock()) - // Test Ping response, err := client.Ping() if err != nil { t.Errorf("Error calling Ping: %v", err) From dcee54f3429c6d03f309d394d769060e8b5a86e4 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 15:49:00 +0300 Subject: [PATCH 24/26] remove comment Signed-off-by: SammyOina --- pkg/bootstrap/bootstrap_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/bootstrap/bootstrap_test.go b/pkg/bootstrap/bootstrap_test.go index 8fd85fb..7341c96 100644 --- a/pkg/bootstrap/bootstrap_test.go +++ b/pkg/bootstrap/bootstrap_test.go @@ -133,7 +133,6 @@ func TestBootstrap(t *testing.T) { assert.True(t, errors.Contains(err, test.expectedErr), fmt.Sprintf("expected %v got %v", test.expectedErr, err)) }) } - // cleanup. t.Cleanup(func() { os.Remove("config.toml") }) From 3cd45213dedd8d9634d4a40e6df0e440dbb0268c Mon Sep 17 00:00:00 2001 From: SammyOina Date: Thu, 28 Sep 2023 16:51:22 +0300 Subject: [PATCH 25/26] Fix Docker image tag in Makefile The Docker image tag in the Makefile was updated to use lowercase "mainflux" instead of "Mainflux". This ensures consistency with the naming convention used for the Docker images. Signed-off-by: SammyOina --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5448bbb..a33efc5 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ define make_docker --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg TIME=$(TIME) \ - --tag=Mainflux/$(svc) \ + --tag=mainflux/$(svc) \ -f docker/Dockerfile . endef From acc18bb310fe3a7458fb004fb3dbcc435c858174 Mon Sep 17 00:00:00 2001 From: SammyOina Date: Wed, 18 Oct 2023 16:07:38 +0300 Subject: [PATCH 26/26] add test cases and format string Signed-off-by: SammyOina --- pkg/bootstrap/bootstrap_test.go | 80 ++++++++++++++++----------------- pkg/terminal/terminal_test.go | 55 ++++++++++++++--------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/pkg/bootstrap/bootstrap_test.go b/pkg/bootstrap/bootstrap_test.go index 7341c96..b94c037 100644 --- a/pkg/bootstrap/bootstrap_test.go +++ b/pkg/bootstrap/bootstrap_test.go @@ -21,24 +21,28 @@ func TestBootstrap(t *testing.T) { http.Error(w, "Invalid authorization header", http.StatusUnauthorized) return } + resp := ` + { + "thing_id": "e22c383a-d2ab-47c1-89cd-903955da993d", + "thing_key": "fc987711-1828-461b-aa4b-16d5b2c642fe", + "channels": [ + %s + ], + "content": "{\"agent\":{\"edgex\":{\"url\":\"http://localhost:48090/api/v1/\"},\"heartbeat\":{\"interval\":\"30s\"},\"log\":{\"level\":\"debug\"},\"mqtt\":{\"mtls\":false,\"qos\":0,\"retain\":false,\"skip_tls_ver\":true,\"url\":\"tcp://mainflux-domain.com:1883\"},\"server\":{\"nats_url\":\"localhost:4222\",\"port\":\"9000\"},\"terminal\":{\"session_timeout\":\"30s\"}},\"export\":{\"exp\":{\"cache_db\":\"0\",\"cache_pass\":\"\",\"cache_url\":\"localhost:6379\",\"log_level\":\"debug\",\"nats\":\"nats://localhost:4222\",\"port\":\"8172\"},\"mqtt\":{\"ca_path\":\"ca.crt\",\"cert_path\":\"thing.crt\",\"channel\":\"\",\"host\":\"tcp://mainflux-domain.com:1883\",\"mtls\":false,\"password\":\"\",\"priv_key_path\":\"thing.key\",\"qos\":0,\"retain\":false,\"skip_tls_ver\":false,\"username\":\"\"},\"routes\":[{\"mqtt_topic\":\"\",\"nats_topic\":\"channels\",\"subtopic\":\"\",\"type\":\"mfx\",\"workers\":10},{\"mqtt_topic\":\"\",\"nats_topic\":\"export\",\"subtopic\":\"\",\"type\":\"default\",\"workers\":10}]}}" + } + ` if r.Header.Get("Authorization") == "Thing invalidChannels" { // Simulate a malformed response. - resp := ` + channels := ` { - "thing_id": "e22c383a-d2ab-47c1-89cd-903955da993d", - "thing_key": "fc987711-1828-461b-aa4b-16d5b2c642fe", - "channels": [ - { - "id": "fa5f9ba8-a1fc-4380-9edb-d0c23eaa24ec", - "name": "control-channel", - "metadata": { - "type": "control" - } - } - ], - "content": "{\"agent\":{\"edgex\":{\"url\":\"http://localhost:48090/api/v1/\"},\"heartbeat\":{\"interval\":\"30s\"},\"log\":{\"level\":\"debug\"},\"mqtt\":{\"mtls\":false,\"qos\":0,\"retain\":false,\"skip_tls_ver\":true,\"url\":\"tcp://mainflux-domain.com:1883\"},\"server\":{\"nats_url\":\"localhost:4222\",\"port\":\"9000\"},\"terminal\":{\"session_timeout\":\"30s\"}},\"export\":{\"exp\":{\"cache_db\":\"0\",\"cache_pass\":\"\",\"cache_url\":\"localhost:6379\",\"log_level\":\"debug\",\"nats\":\"nats://localhost:4222\",\"port\":\"8172\"},\"mqtt\":{\"ca_path\":\"ca.crt\",\"cert_path\":\"thing.crt\",\"channel\":\"\",\"host\":\"tcp://mainflux-domain.com:1883\",\"mtls\":false,\"password\":\"\",\"priv_key_path\":\"thing.key\",\"qos\":0,\"retain\":false,\"skip_tls_ver\":false,\"username\":\"\"},\"routes\":[{\"mqtt_topic\":\"\",\"nats_topic\":\"channels\",\"subtopic\":\"\",\"type\":\"mfx\",\"workers\":10},{\"mqtt_topic\":\"\",\"nats_topic\":\"export\",\"subtopic\":\"\",\"type\":\"default\",\"workers\":10}]}}" - } + "id": "fa5f9ba8-a1fc-4380-9edb-d0c23eaa24ec", + "name": "control-channel", + "metadata": { + "type": "control" + } + } ` + resp = fmt.Sprintf(resp, channels) w.WriteHeader(http.StatusOK) if _, err := io.WriteString(w, resp); err != nil { t.Errorf(err.Error()) @@ -46,36 +50,30 @@ func TestBootstrap(t *testing.T) { return } // Simulate a successful response. - resp := ` + channels := ` { - "thing_id": "e22c383a-d2ab-47c1-89cd-903955da993d", - "thing_key": "fc987711-1828-461b-aa4b-16d5b2c642fe", - "channels": [ - { - "id": "fa5f9ba8-a1fc-4380-9edb-d0c23eaa24ec", - "name": "control-channel", - "metadata": { - "type": "control" - } - }, - { - "id": "24e5473e-3cbe-43d9-8a8b-a725ff918c0e", - "name": "data-channel", - "metadata": { - "type": "data" - } - }, - { - "id": "1eac45c2-0f72-4089-b255-ebd2e5732bbb", - "name": "export-channel", - "metadata": { - "type": "export" - } - } - ], - "content": "{\"agent\":{\"edgex\":{\"url\":\"http://localhost:48090/api/v1/\"},\"heartbeat\":{\"interval\":\"30s\"},\"log\":{\"level\":\"debug\"},\"mqtt\":{\"mtls\":false,\"qos\":0,\"retain\":false,\"skip_tls_ver\":true,\"url\":\"tcp://mainflux-domain.com:1883\"},\"server\":{\"nats_url\":\"localhost:4222\",\"port\":\"9000\"},\"terminal\":{\"session_timeout\":\"30s\"}},\"export\":{\"exp\":{\"cache_db\":\"0\",\"cache_pass\":\"\",\"cache_url\":\"localhost:6379\",\"log_level\":\"debug\",\"nats\":\"nats://localhost:4222\",\"port\":\"8172\"},\"mqtt\":{\"ca_path\":\"ca.crt\",\"cert_path\":\"thing.crt\",\"channel\":\"\",\"host\":\"tcp://mainflux-domain.com:1883\",\"mtls\":false,\"password\":\"\",\"priv_key_path\":\"thing.key\",\"qos\":0,\"retain\":false,\"skip_tls_ver\":false,\"username\":\"\"},\"routes\":[{\"mqtt_topic\":\"\",\"nats_topic\":\"channels\",\"subtopic\":\"\",\"type\":\"mfx\",\"workers\":10},{\"mqtt_topic\":\"\",\"nats_topic\":\"export\",\"subtopic\":\"\",\"type\":\"default\",\"workers\":10}]}}" + "id": "fa5f9ba8-a1fc-4380-9edb-d0c23eaa24ec", + "name": "control-channel", + "metadata": { + "type": "control" + } + }, + { + "id": "24e5473e-3cbe-43d9-8a8b-a725ff918c0e", + "name": "data-channel", + "metadata": { + "type": "data" + } + }, + { + "id": "1eac45c2-0f72-4089-b255-ebd2e5732bbb", + "name": "export-channel", + "metadata": { + "type": "export" + } } ` + resp = fmt.Sprintf(resp, channels) w.WriteHeader(http.StatusOK) if _, err := io.WriteString(w, resp); err != nil { t.Errorf(err.Error()) diff --git a/pkg/terminal/terminal_test.go b/pkg/terminal/terminal_test.go index 6c06e51..0b7bc63 100644 --- a/pkg/terminal/terminal_test.go +++ b/pkg/terminal/terminal_test.go @@ -1,6 +1,7 @@ package terminal import ( + "errors" "testing" "time" @@ -14,31 +15,44 @@ const ( ) // MockPublish is a mock function for the publish function used in NewSession. -func MockPublish(channel, payload string) error { +func mockPublish(channel, payload string) error { return nil } -func TestSessionWrite(t *testing.T) { - publish := MockPublish +func mockPublishFail(channel, payload string) error { + return errors.New("") +} - session, err := NewSession(uuid, timeout, publish, logger.NewMock()) - if err != nil { - t.Fatalf("Expected no error, but got: %v", err) - } +func TestWrite(t *testing.T) { + t.Run("successful publish", func(t *testing.T) { + session, err := NewSession(uuid, timeout, mockPublish, logger.NewMock()) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } - // Simulate writing data to the session - data := []byte("test data") - n, err := session.Write(data) - if err != nil { - t.Fatalf("Expected no error, but got: %v", err) - } - assert.Equal(t, len(data), n) -} + // Simulate writing data to the session + data := []byte("test data") + n, err := session.Write(data) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + assert.Equal(t, len(data), n) + }) + t.Run("failed publish", func(t *testing.T) { + session, err := NewSession(uuid, timeout, mockPublishFail, logger.NewMock()) + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } -func TestSessionSend(t *testing.T) { - publish := MockPublish + // Simulate writing data to the session + data := []byte("test data") + _, err = session.Write(data) + assert.NotNil(t, err) + }) +} - session, err := NewSession(uuid, timeout, publish, logger.NewMock()) +func TestSend(t *testing.T) { + session, err := NewSession(uuid, timeout, mockPublish, logger.NewMock()) if err != nil { t.Fatalf("Expected no error, but got: %v", err) } @@ -49,10 +63,11 @@ func TestSessionSend(t *testing.T) { if err = session.Send(data); err != nil { t.Fatalf("Expected no error, but got: %v", err) } + } -func TestSessionIsDone(t *testing.T) { - publish := MockPublish +func TestIsDone(t *testing.T) { + publish := mockPublish session, err := NewSession(uuid, timeout, publish, logger.NewMock()) if err != nil {