diff --git a/agent/container/pkg/clients/mock_nats_client.go b/agent/container/pkg/clients/mock_nats_client.go new file mode 100644 index 00000000..5f26bbd1 --- /dev/null +++ b/agent/container/pkg/clients/mock_nats_client.go @@ -0,0 +1,76 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: /Users/vijeshdeepan/Desktop/kubviz/agent/container/pkg/clients/nats_client.go + +// Package clients is a generated GoMock package. +package clients + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + nats "github.com/nats-io/nats.go" +) + +// MockNATSClientInterface is a mock of NATSClientInterface interface. +type MockNATSClientInterface struct { + ctrl *gomock.Controller + recorder *MockNATSClientInterfaceMockRecorder +} + +// MockNATSClientInterfaceMockRecorder is the mock recorder for MockNATSClientInterface. +type MockNATSClientInterfaceMockRecorder struct { + mock *MockNATSClientInterface +} + +// NewMockNATSClientInterface creates a new mock instance. +func NewMockNATSClientInterface(ctrl *gomock.Controller) *MockNATSClientInterface { + mock := &MockNATSClientInterface{ctrl: ctrl} + mock.recorder = &MockNATSClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNATSClientInterface) EXPECT() *MockNATSClientInterfaceMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockNATSClientInterface) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockNATSClientInterfaceMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockNATSClientInterface)(nil).Close)) +} + +// CreateStream mocks base method. +func (m *MockNATSClientInterface) CreateStream() (nats.JetStreamContext, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateStream") + ret0, _ := ret[0].(nats.JetStreamContext) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateStream indicates an expected call of CreateStream. +func (mr *MockNATSClientInterfaceMockRecorder) CreateStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStream", reflect.TypeOf((*MockNATSClientInterface)(nil).CreateStream)) +} + +// Publish mocks base method. +func (m *MockNATSClientInterface) Publish(event []byte, repo string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Publish", event, repo) + ret0, _ := ret[0].(error) + return ret0 +} + +// Publish indicates an expected call of Publish. +func (mr *MockNATSClientInterfaceMockRecorder) Publish(event, repo interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockNATSClientInterface)(nil).Publish), event, repo) +} diff --git a/agent/container/pkg/clients/nats_client.go b/agent/container/pkg/clients/nats_client.go index d087bb6c..cef9d0ba 100755 --- a/agent/container/pkg/clients/nats_client.go +++ b/agent/container/pkg/clients/nats_client.go @@ -15,6 +15,12 @@ import ( "github.com/nats-io/nats.go" ) +type NATSClientInterface interface { + Close() + CreateStream() (nats.JetStreamContext, error) + Publish(event []byte, repo string) error +} + // constant variables to use with nats stream and // nats publishing const ( diff --git a/agent/container/pkg/handler/api_handler.go b/agent/container/pkg/handler/api_handler.go index 6efee78d..7df98def 100755 --- a/agent/container/pkg/handler/api_handler.go +++ b/agent/container/pkg/handler/api_handler.go @@ -1,18 +1,26 @@ package handler import ( + "encoding/json" + "errors" + "fmt" + "io" "log" "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/intelops/kubviz/agent/container/api" "github.com/intelops/kubviz/agent/container/pkg/clients" + "github.com/intelops/kubviz/model" "github.com/intelops/kubviz/pkg/opentelemetry" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" ) type APIHandler struct { - conn *clients.NATSContext + conn clients.NATSClientInterface } const ( @@ -56,6 +64,7 @@ func (ah *APIHandler) BindRequest(r *gin.Engine) { // This endpoint can be used by tools like Swagger UI to provide interactive documentation for the API. func (ah *APIHandler) GetApiDocs(c *gin.Context) { swagger, err := api.GetSwagger() + fmt.Println(swagger) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -73,3 +82,157 @@ func (ah *APIHandler) GetStatus(c *gin.Context) { c.Header(contentType, appJSONContentType) c.Status(http.StatusOK) } + +var ErrInvalidPayload = errors.New("invalid or malformed Azure Container Registry webhook payload") + +// PostEventAzureContainer listens for Azure Container Registry image push events. +// When a new image is pushed, this endpoint receives the event payload, validates it, +// and then publishes it to a NATS messaging system. This allows client of the +// application to subscribe to these events and respond to changes in the container registry. +// If the payload is invalid or the publishing process fails, an error response is returned. +func (ah *APIHandler) PostEventAzureContainer(c *gin.Context) { + + tracer := otel.Tracer("azure-container") + _, span := tracer.Start(c.Request.Context(), "PostEventAzureContainer") + span.SetAttributes(attribute.String("http.method", "POST")) + defer span.End() + + defer func() { + _, _ = io.Copy(io.Discard, c.Request.Body) + _ = c.Request.Body.Close() + }() + payload, err := io.ReadAll(c.Request.Body) + if err != nil || len(payload) == 0 { + log.Printf("%v: %v", ErrReadingBody, err) + c.Status(http.StatusBadRequest) + return + } + + var pushEvent model.AzureContainerPushEventPayload + err = json.Unmarshal(payload, &pushEvent) + if err != nil { + log.Printf("%v: %v", ErrInvalidPayload, err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) + return + } + + log.Printf("Received event from Azure Container Registry: %v", pushEvent) + + err = ah.conn.Publish(payload, "Azure_Container_Registry") + if err != nil { + log.Printf("%v: %v", ErrPublishToNats, err) + c.Status(http.StatusInternalServerError) + return + } + c.Status(http.StatusOK) +} + +// parse errors +var ( + ErrReadingBody = errors.New("error reading the request body") + ErrPublishToNats = errors.New("error while publishing to nats") +) + +func (ah *APIHandler) PostEventDockerHub(c *gin.Context) { + + tracer := otel.Tracer("dockerhub-container") + _, span := tracer.Start(c.Request.Context(), "PostEventDockerHub") + span.SetAttributes(attribute.String("http.method", "POST")) + defer span.End() + + payload, err := io.ReadAll(c.Request.Body) + if err != nil { + log.Printf("%v: %v", ErrReadingBody, err) + c.Status(http.StatusBadRequest) + return + } + if len(payload) == 0 || strings.TrimSpace(string(payload)) == "" { + log.Printf("%v: %v", ErrReadingBody, "empty body") + c.Status(http.StatusBadRequest) + return + } + log.Printf("Received event from docker artifactory: %v", string(payload)) + err = ah.conn.Publish(payload, "Dockerhub_Registry") + if err != nil { + log.Printf("%v: %v", ErrPublishToNats, err) + c.AbortWithStatus(http.StatusInternalServerError) // Use AbortWithStatus + return + } + c.Status(http.StatusOK) +} + +var ErrInvalidPayloads = errors.New("invalid or malformed jfrog Container Registry webhook payload") + +func (ah *APIHandler) PostEventJfrogContainer(c *gin.Context) { + + tracer := otel.Tracer("jfrog-container") + _, span := tracer.Start(c.Request.Context(), "PostEventJfrogContainer") + span.SetAttributes(attribute.String("http.method", "POST")) + defer span.End() + + payload, err := io.ReadAll(c.Request.Body) + if err != nil { + log.Printf("%v: %v", ErrReadingBody, err) + c.Status(http.StatusBadRequest) + return + } + if len(payload) == 0 || strings.TrimSpace(string(payload)) == "" { + log.Printf("%v: %v", ErrReadingBody, "empty body") + c.Status(http.StatusBadRequest) + return + } + + var pushEvent model.JfrogContainerPushEventPayload + err = json.Unmarshal(payload, &pushEvent) + if err != nil { + log.Printf("%v: %v", ErrInvalidPayloads, err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) + return + } + + log.Printf("Received event from jfrog Container Registry: %v", pushEvent) + + err = ah.conn.Publish(payload, "Jfrog_Container_Registry") + if err != nil { + log.Printf("%v: %v", ErrPublishToNats, err) + c.AbortWithStatus(http.StatusInternalServerError) // Use AbortWithStatus + return + } + c.Status(http.StatusOK) +} + +func (ah *APIHandler) PostEventQuayContainer(c *gin.Context) { + + tracer := otel.Tracer("quay-container") + _, span := tracer.Start(c.Request.Context(), "PostEventQuayContainer") + span.SetAttributes(attribute.String("http.method", "POST")) + defer span.End() + + payload, err := io.ReadAll(c.Request.Body) + if err != nil { + log.Printf("%v: %v", ErrReadingBody, err) + c.Status(http.StatusBadRequest) + return + } + if len(payload) == 0 || strings.TrimSpace(string(payload)) == "" { + log.Printf("%v: %v", ErrReadingBody, "empty body") + c.Status(http.StatusBadRequest) + return + } + var pushEvent model.QuayImagePushPayload + err = json.Unmarshal(payload, &pushEvent) + if err != nil { + log.Printf("%v: %v", "invalid or malformed Quay Container Registry webhook payload", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) + return + } + log.Printf("Received event from Quay Container Registry: %v", pushEvent) + + err = ah.conn.Publish(payload, "Quay_Container_Registry") + if err != nil { + log.Printf("%v: %v", ErrPublishToNats, err) + c.AbortWithStatus(http.StatusInternalServerError) // Use AbortWithStatus + return + } + c.Status(http.StatusOK) +} diff --git a/agent/container/pkg/handler/api_handler_test.go b/agent/container/pkg/handler/api_handler_test.go new file mode 100644 index 00000000..22a25881 --- /dev/null +++ b/agent/container/pkg/handler/api_handler_test.go @@ -0,0 +1,385 @@ +package handler + +import ( + "bytes" + "errors" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/agiledragon/gomonkey" + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" + "github.com/golang/mock/gomock" + "github.com/intelops/kubviz/agent/container/api" + mock_main "github.com/intelops/kubviz/agent/container/pkg/clients" + "github.com/stretchr/testify/assert" +) + +func TestGetLiveness(t *testing.T) { + gin.SetMode(gin.TestMode) + app := &APIHandler{} + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + app.GetStatus(c) + assert.Equal(t, http.StatusOK, w.Code) +} +func TestGetApiDocs(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create an instance of the Application struct + app := &APIHandler{} + + // Define the test cases + tests := []struct { + name string + mockResponse *openapi3.T + mockError error + expectedCode int + expectedBody string + }{ + { + name: "Success", + mockResponse: &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "Sample API", + Version: "1.0.0", + }, + Paths: openapi3.Paths{}, + }, + mockError: nil, + expectedCode: http.StatusOK, + expectedBody: `{"openapi":"3.0.0","info":{"title":"Sample API","version":"1.0.0"},"paths":{}}`, + }, + { + name: "Error", + mockResponse: nil, + mockError: errors.New("error fetching swagger"), + expectedCode: http.StatusInternalServerError, + expectedBody: `{"error":"error fetching swagger"}`, // Updated to match the actual error response + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Patch the GetSwagger function + patch := gomonkey.ApplyFunc(api.GetSwagger, func() (*openapi3.T, error) { + return tt.mockResponse, tt.mockError + }) + defer patch.Reset() + + // Create a new Gin context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Call the GetApiDocs method + app.GetApiDocs(c) + + // Verify the response + assert.Equal(t, tt.expectedCode, w.Code) + assert.JSONEq(t, tt.expectedBody, w.Body.String()) + }) + } +} + +func TestPostEventAzureContainer(t *testing.T) { + gin.SetMode(gin.TestMode) + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockNatsClient := mock_main.NewMockNATSClientInterface(mockCtrl) + app := &APIHandler{ + conn: mockNatsClient, + } + + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Valid request", + headerEvent: "event", + bodyData: []byte(`{"id":"123","timestamp":"2024-06-10T10:00:00Z","action":"push","target":{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":123,"digest":"sha256:1234567890abcdef","length":123,"repository":"repo","tag":"latest"},"request":{"id":"456","host":"localhost","method":"GET","useragent":"curl"}}`), + expectedLog: "Received event from Azure Container Registry", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset the recorder for each test case + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Set the request body and header + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + req.Header.Set("X-Event", tt.headerEvent) + c.Request = req + + // Set the expectation on the mock + if tt.mockPublishErr != nil { + mockNatsClient.EXPECT().Publish(gomock.Any(), gomock.Any()).Return(tt.mockPublishErr) + } else { + mockNatsClient.EXPECT().Publish(gomock.Any(), gomock.Any()).Return(nil) + } + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + // Perform the request + app.PostEventAzureContainer(c) + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Check the response status code + assert.Equal(t, tt.expectedStatus, w.Code) + }) + } +} + +func TestPostEventDockerHub(t *testing.T) { + gin.SetMode(gin.TestMode) + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockNatsClient := mock_main.NewMockNATSClientInterface(mockCtrl) + app := &APIHandler{ + conn: mockNatsClient, + } + + // Define test cases + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Valid request", + headerEvent: "event", + bodyData: []byte(`{"key": "value"}`), + expectedLog: "Received event from docker artifactory:", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Empty body", + headerEvent: "event", + bodyData: []byte{}, + expectedLog: "error reading the request body", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Error publishing to NATS", + headerEvent: "event", + bodyData: []byte(`{"key": "value"}`), + expectedLog: "error while publishing to nats", + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("some error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset the recorder for each test case + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Set the request body and header + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + req.Header.Set("X-Event", tt.headerEvent) + c.Request = req + + // Set the mock expectation only if the body is not empty + if len(tt.bodyData) > 0 { + mockNatsClient.EXPECT().Publish(gomock.Any(), gomock.Any()).Return(tt.mockPublishErr) + } + + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + // Perform the request + app.PostEventDockerHub(c) + + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Check the response status code + assert.Equal(t, tt.expectedStatus, w.Code) + + // Log the error message and request body for debugging + if w.Code != tt.expectedStatus { + t.Log("Response body:", w.Body.String()) + t.Log("Request body:", string(tt.bodyData)) + } + + }) + } +} + +func TestPostEventJfrogContainer(t *testing.T) { + gin.SetMode(gin.TestMode) + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockNatsClient := mock_main.NewMockNATSClientInterface(mockCtrl) + app := &APIHandler{ + conn: mockNatsClient, + } + + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Valid request", + headerEvent: "event", + bodyData: []byte(`{"domain":"domain","event_type":"event","data":{"repo_key":"key","path":"path","name":"name","sha256":"sha","size":123,"image_name":"image","tag":"tag"},"subscription_key":"sub","jpd_origin":"origin","source":"source"}`), + expectedLog: "Received event from jfrog Container Registry", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Empty body", + headerEvent: "event", + bodyData: []byte{}, + expectedLog: "error reading the request body", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Error publishing to NATS", + headerEvent: "event", + bodyData: []byte(`{"domain":"domain","event_type":"event","data":{"repo_key":"key","path":"path","name":"name","sha256":"sha","size":123,"image_name":"image","tag":"tag"},"subscription_key":"sub","jpd_origin":"origin","source":"source"}`), + expectedLog: "Received event from jfrog Container", + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("some error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + req.Header.Set("X-Event", tt.headerEvent) + c.Request = req + + // Set the mock expectation only if the body is not empty + if len(tt.bodyData) > 0 { + mockNatsClient.EXPECT().Publish(gomock.Any(), gomock.Any()).Return(tt.mockPublishErr) + } + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + app.PostEventJfrogContainer(c) + + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + assert.Equal(t, tt.expectedStatus, w.Code) + + if w.Code != tt.expectedStatus { + t.Log("Response body:", w.Body.String()) + t.Log("Request body:", string(tt.bodyData)) + } + }) + } +} + +func TestPostEventQuayContainer(t *testing.T) { + gin.SetMode(gin.TestMode) + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockNatsClient := mock_main.NewMockNATSClientInterface(mockCtrl) + app := &APIHandler{ + conn: mockNatsClient, + } + + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Valid request", + headerEvent: "event", + bodyData: []byte(`{"name":"name","repository":"repo","namespace":"namespace","docker_url":"url","homepage":"home","updated_tags":["tag1","tag2"]}`), + expectedLog: "Received event from Quay Container Registry", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Empty body", + headerEvent: "event", + bodyData: []byte{}, + expectedLog: "error reading the request body", + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Error publishing to NATS", + headerEvent: "event", + bodyData: []byte(`{"name":"name","repository":"repo","namespace":"namespace","docker_url":"url","homepage":"home","updated_tags":["tag1","tag2"]}`), + expectedLog: "Received event from Quay Container", + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("some error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + req, _ := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + req.Header.Set("X-Event", tt.headerEvent) + c.Request = req + + // Set the mock expectation only if the body is not empty + if len(tt.bodyData) > 0 { + mockNatsClient.EXPECT().Publish(gomock.Any(), gomock.Any()).Return(tt.mockPublishErr) + } + + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + app.PostEventQuayContainer(c) + + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + assert.Equal(t, tt.expectedStatus, w.Code) + + if w.Code != tt.expectedStatus { + t.Log("Response body:", w.Body.String()) + t.Log("Request body:", string(tt.bodyData)) + } + }) + } +} diff --git a/agent/container/pkg/handler/azure_container.go b/agent/container/pkg/handler/azure_container.go deleted file mode 100644 index 35a72f3f..00000000 --- a/agent/container/pkg/handler/azure_container.go +++ /dev/null @@ -1,58 +0,0 @@ -package handler - -import ( - "encoding/json" - "errors" - "io" - "log" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/intelops/kubviz/model" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" -) - -var ErrInvalidPayload = errors.New("invalid or malformed Azure Container Registry webhook payload") - -// PostEventAzureContainer listens for Azure Container Registry image push events. -// When a new image is pushed, this endpoint receives the event payload, validates it, -// and then publishes it to a NATS messaging system. This allows client of the -// application to subscribe to these events and respond to changes in the container registry. -// If the payload is invalid or the publishing process fails, an error response is returned. -func (ah *APIHandler) PostEventAzureContainer(c *gin.Context) { - - tracer := otel.Tracer("azure-container") - _, span := tracer.Start(c.Request.Context(), "PostEventAzureContainer") - span.SetAttributes(attribute.String("http.method", "POST")) - defer span.End() - - defer func() { - _, _ = io.Copy(io.Discard, c.Request.Body) - _ = c.Request.Body.Close() - }() - payload, err := io.ReadAll(c.Request.Body) - if err != nil || len(payload) == 0 { - log.Printf("%v: %v", ErrReadingBody, err) - c.Status(http.StatusBadRequest) - return - } - - var pushEvent model.AzureContainerPushEventPayload - err = json.Unmarshal(payload, &pushEvent) - if err != nil { - log.Printf("%v: %v", ErrInvalidPayload, err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) - return - } - - log.Printf("Received event from Azure Container Registry: %v", pushEvent) - - err = ah.conn.Publish(payload, "Azure_Container_Registry") - if err != nil { - log.Printf("%v: %v", ErrPublishToNats, err) - c.Status(http.StatusInternalServerError) - return - } - c.Status(http.StatusOK) -} diff --git a/agent/container/pkg/handler/docker_event_dockerhub.go b/agent/container/pkg/handler/docker_event_dockerhub.go deleted file mode 100644 index f74bd8ae..00000000 --- a/agent/container/pkg/handler/docker_event_dockerhub.go +++ /dev/null @@ -1,45 +0,0 @@ -package handler - -import ( - "errors" - "io" - "log" - "net/http" - - "github.com/gin-gonic/gin" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" -) - -// parse errors -var ( - ErrReadingBody = errors.New("error reading the request body") - ErrPublishToNats = errors.New("error while publishing to nats") -) - -func (ah *APIHandler) PostEventDockerHub(c *gin.Context) { - - tracer := otel.Tracer("dockerhub-container") - _, span := tracer.Start(c.Request.Context(), "PostEventDockerHub") - span.SetAttributes(attribute.String("http.method", "POST")) - defer span.End() - - defer func() { - _, _ = io.Copy(io.Discard, c.Request.Body) - _ = c.Request.Body.Close() - }() - payload, err := io.ReadAll(c.Request.Body) - if err != nil || len(payload) == 0 { - log.Printf("%v: %v", ErrReadingBody, err) - c.Status(http.StatusBadRequest) - return - } - log.Printf("Received event from docker artifactory: %v", string(payload)) - err = ah.conn.Publish(payload, "Dockerhub_Registry") - if err != nil { - log.Printf("%v: %v", ErrPublishToNats, err) - c.Status(http.StatusInternalServerError) - return - } - c.Status(http.StatusOK) -} diff --git a/agent/container/pkg/handler/jfrog_container.go b/agent/container/pkg/handler/jfrog_container.go deleted file mode 100644 index 8d57f272..00000000 --- a/agent/container/pkg/handler/jfrog_container.go +++ /dev/null @@ -1,53 +0,0 @@ -package handler - -import ( - "encoding/json" - "errors" - "io" - "log" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/intelops/kubviz/model" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" -) - -var ErrInvalidPayloads = errors.New("invalid or malformed jfrog Container Registry webhook payload") - -func (ah *APIHandler) PostEventJfrogContainer(c *gin.Context) { - - tracer := otel.Tracer("jfrog-container") - _, span := tracer.Start(c.Request.Context(), "PostEventJfrogContainer") - span.SetAttributes(attribute.String("http.method", "POST")) - defer span.End() - - defer func() { - _, _ = io.Copy(io.Discard, c.Request.Body) - _ = c.Request.Body.Close() - }() - payload, err := io.ReadAll(c.Request.Body) - if err != nil || len(payload) == 0 { - log.Printf("%v: %v", ErrReadingBody, err) - c.Status(http.StatusBadRequest) - return - } - - var pushEvent model.JfrogContainerPushEventPayload - err = json.Unmarshal(payload, &pushEvent) - if err != nil { - log.Printf("%v: %v", ErrInvalidPayloads, err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) - return - } - - log.Printf("Received event from jfrog Container Registry: %v", pushEvent) - - err = ah.conn.Publish(payload, "Jfrog_Container_Registry") - if err != nil { - log.Printf("%v: %v", ErrPublishToNats, err) - c.Status(http.StatusInternalServerError) - return - } - c.Status(http.StatusOK) -} diff --git a/agent/container/pkg/handler/quay_handler.go b/agent/container/pkg/handler/quay_handler.go deleted file mode 100644 index b1a2be84..00000000 --- a/agent/container/pkg/handler/quay_handler.go +++ /dev/null @@ -1,48 +0,0 @@ -package handler - -import ( - "encoding/json" - "io" - "log" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/intelops/kubviz/model" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" -) - -func (ah *APIHandler) PostEventQuayContainer(c *gin.Context) { - - tracer := otel.Tracer("quay-container") - _, span := tracer.Start(c.Request.Context(), "PostEventQuayContainer") - span.SetAttributes(attribute.String("http.method", "POST")) - defer span.End() - - defer func() { - _, _ = io.Copy(io.Discard, c.Request.Body) - _ = c.Request.Body.Close() - }() - payload, err := io.ReadAll(c.Request.Body) - if err != nil || len(payload) == 0 { - log.Printf("%v: %v", ErrReadingBody, err) - c.Status(http.StatusBadRequest) - return - } - var pushEvent model.QuayImagePushPayload - err = json.Unmarshal(payload, &pushEvent) - if err != nil { - log.Printf("%v: %v", ErrInvalidPayload, err) - c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"}) - return - } - log.Printf("Received event from Quay Container Registry: %v", pushEvent) - - err = ah.conn.Publish(payload, "Quay_Container_Registry") - if err != nil { - log.Printf("%v: %v", ErrPublishToNats, err) - c.Status(http.StatusInternalServerError) - return - } - c.Status(http.StatusOK) -} \ No newline at end of file diff --git a/agent/git/pkg/application/application.go b/agent/git/pkg/application/application.go index f8bd908d..ce979344 100644 --- a/agent/git/pkg/application/application.go +++ b/agent/git/pkg/application/application.go @@ -20,7 +20,7 @@ import ( type Application struct { Config *config.Config server *http.Server - conn *clients.NATSContext + conn clients.NATSClientInterface } func New(conf *config.Config, conn *clients.NATSContext) *Application { diff --git a/agent/git/pkg/application/handlers_test.go b/agent/git/pkg/application/handlers_test.go new file mode 100644 index 00000000..3ae711e7 --- /dev/null +++ b/agent/git/pkg/application/handlers_test.go @@ -0,0 +1,650 @@ +package application + +import ( + "bytes" + "errors" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/agiledragon/gomonkey" + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" + "github.com/golang/mock/gomock" + "github.com/intelops/kubviz/agent/git/api" + "github.com/intelops/kubviz/agent/git/pkg/clients" + "github.com/intelops/kubviz/agent/git/pkg/clients/mocks" + "github.com/intelops/kubviz/agent/git/pkg/config" + "github.com/intelops/kubviz/model" + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetApiDocs(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create an instance of the Application struct + app := &Application{} + + // Define the test cases + tests := []struct { + name string + mockResponse *openapi3.T + mockError error + expectedCode int + expectedBody string + }{ + { + name: "Success", + mockResponse: &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + Title: "Sample API", + Version: "1.0.0", + }, + Paths: openapi3.Paths{}, + }, + mockError: nil, + expectedCode: http.StatusOK, + expectedBody: `{"openapi":"3.0.0","info":{"title":"Sample API","version":"1.0.0"},"paths":{}}`, + }, + { + name: "Error", + mockResponse: nil, + mockError: errors.New("error fetching swagger"), + expectedCode: http.StatusInternalServerError, + expectedBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Patch the GetSwagger function + patch := gomonkey.ApplyFunc(api.GetSwagger, func() (*openapi3.T, error) { + return tt.mockResponse, tt.mockError + }) + defer patch.Reset() + + // Create a new Gin context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Call the GetApiDocs method + app.GetApiDocs(c) + + // Verify the response + assert.Equal(t, tt.expectedCode, w.Code) + if tt.expectedCode == http.StatusOK { + assert.JSONEq(t, tt.expectedBody, w.Body.String()) + } else { + assert.Empty(t, w.Body.String()) + } + }) + } +} + +func TestGetLiveness(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create an instance of the Application struct + app := &Application{} + + // Create a new Gin context + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + // Call the GetLiveness method + app.GetLiveness(c) + + // Verify the response + assert.Equal(t, http.StatusOK, w.Code) +} +func TestNew(t *testing.T) { + // Test case: valid configuration and NATS connection + conf := &config.Config{Port: 8080} + conn := &clients.NATSContext{} + app := New(conf, conn) + if app.Config != conf { + t.Errorf("Expected Config to be %v, got %v", conf, app.Config) + } + if app.conn != conn { + t.Errorf("Expected conn to be %v, got %v", conn, app.conn) + } + if app.server.Addr != ":8081" { + t.Errorf("Expected server.Addr to be :8081, got %s", app.server.Addr) + } + if app.server.Handler == nil { + t.Error("Expected server.Handler to be non-nil") + } + if app.server.IdleTimeout != time.Minute { + t.Errorf("Expected server.IdleTimeout to be %v, got %v", time.Minute, app.server.IdleTimeout) + } + if app.server.ReadTimeout != 10*time.Second { + t.Errorf("Expected server.ReadTimeout to be %v, got %v", 10*time.Second, app.server.ReadTimeout) + } + if app.server.WriteTimeout != 30*time.Second { + t.Errorf("Expected server.WriteTimeout to be %v, got %v", 30*time.Second, app.server.WriteTimeout) + } + + // Test case: nil configuration + app = New(nil, conn) + if app.Config != nil { + t.Errorf("Expected Config to be nil, got %v", app.Config) + } + + // Test case: nil NATS connection + app = New(conf, nil) + if app.conn != nil { + t.Errorf("Expected conn to be nil, got %v", app.conn) + } +} + +func TestStart(t *testing.T) { + // Create an instance of the Application struct + app := &Application{ + server: &http.Server{Addr: ":8080"}, // Initialize app.server with a valid http.Server instance + } + + // Create a test server + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Respond with a success status code + w.WriteHeader(http.StatusOK) + })) + defer testServer.Close() + + // Start the server in a goroutine + go func() { + // Pass the test server's URL to ListenAndServe + if err := app.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + t.Fatalf("Server closed, reason: %v", err) + } + }() + + // Wait for the server to start + time.Sleep(100 * time.Millisecond) + + // Make a request to the server + resp, err := http.Get(testServer.URL) + if err != nil { + t.Fatalf("Failed to make GET request: %v", err) + } + defer resp.Body.Close() + + // Verify the response + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +type MockNATSContext struct { + clients.NATSContext + *http.Server + mock.Mock +} + +func (m *MockNATSContext) Close() { + m.Called() +} + +func (m *MockNATSContext) CreateStream() (nats.JetStreamContext, error) { + args := m.Called() + return args.Get(0).(nats.JetStreamContext), args.Error(1) +} + +func (m *MockNATSContext) Publish(metric []byte, repo string, eventkey model.EventKey, eventvalue model.EventValue) error { + args := m.Called(metric, repo, eventkey, eventvalue) + return args.Error(0) +} + +// Helper type to capture log output + +// Mock the connection interface +func TestPostGitea(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a new mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock NATSContext + mockConn := mocks.NewMockNATSClientInterface(ctrl) + // Create an instance of the Application struct + app := &Application{conn: mockConn} + + // Define the test cases + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Success", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `GITEA DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Missing Event Header", + headerEvent: "", + bodyData: nil, + expectedLog: "error getting the gitea event from header", + expectedStatus: http.StatusBadRequest, + mockPublishErr: nil, + }, + { + name: "Publish Error", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `GITEA DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("publish error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup the mock response + if tt.headerEvent != "" { + mockConn.EXPECT().Publish(tt.bodyData, string(model.GiteaProvider), model.GiteaHeader, model.EventValue(tt.headerEvent)).Return(tt.mockPublishErr).Times(1) + } + + // Create a new Gin context with the necessary headers and body + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + c.Request.Header.Set(string(model.GiteaHeader), tt.headerEvent) + + // Capture logs + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + // Call the PostGitea method + app.PostGitea(c) + + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Verify the response status code + if tt.mockPublishErr != nil { + assert.Equal(t, tt.expectedStatus, http.StatusInternalServerError) + } else if tt.headerEvent == "" { + assert.Equal(t, tt.expectedStatus, http.StatusBadRequest) + } else { + assert.Equal(t, tt.expectedStatus, w.Code) + } + }) + } +} +func TestPostAzure(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a new mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock NATSContext + mockConn := mocks.NewMockNATSClientInterface(ctrl) + // Create an instance of the Application struct + app := &Application{conn: mockConn} + + // Define the test cases + tests := []struct { + name string + bodyData []byte + eventType string + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Success", + bodyData: []byte(`{"eventType": "push"}`), + eventType: "push", + expectedLog: `AZURE DATA: "{\"eventType\": \"push\"}"`, + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Missing EventType", + bodyData: []byte(`{}`), + eventType: "", + expectedLog: "Error Reading Request Body", + expectedStatus: http.StatusInternalServerError, + mockPublishErr: nil, + }, + { + name: "Unmarshal Error", + bodyData: []byte(`invalid json`), + eventType: "", + expectedLog: "Error Reading Request Body", + expectedStatus: http.StatusInternalServerError, + mockPublishErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup the mock response + if tt.eventType != "" { + mockConn.EXPECT().Publish(tt.bodyData, string(model.AzureDevopsProvider), model.AzureHeader, model.EventValue(tt.eventType)).Return(tt.mockPublishErr).Times(1) + } + + // Create a new Gin context with the necessary headers and body + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + + // Capture logs + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + // Call the PostAzure method + app.PostAzure(c) + + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Verify the response status code + if tt.eventType == "" { + assert.Equal(t, tt.expectedStatus, http.StatusInternalServerError) + } else { + assert.Equal(t, tt.expectedStatus, w.Code) + } + }) + } +} +func TestPostGithub(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a new mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock NATSContext + mockConn := mocks.NewMockNATSClientInterface(ctrl) + // Create an instance of the Application struct + app := &Application{conn: mockConn} + + // Define the test cases + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Success", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `GITHUB DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Missing Event Header", + headerEvent: "", + bodyData: nil, + expectedLog: "error getting the github event from header", + expectedStatus: http.StatusBadRequest, + mockPublishErr: nil, + }, + { + name: "Publish Error", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `GITHUB DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("publish error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup the mock response + if tt.headerEvent != "" { + mockConn.EXPECT().Publish(tt.bodyData, string(model.GithubProvider), model.GithubHeader, model.EventValue(tt.headerEvent)).Return(tt.mockPublishErr).Times(1) + } + + // Create a new Gin context with the necessary headers and body + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + c.Request.Header.Set(string(model.GithubHeader), tt.headerEvent) + + // Capture logs + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + // Call the PostGithub method + app.PostGithub(c) + + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Verify the response status code + if tt.mockPublishErr != nil { + assert.Equal(t, tt.expectedStatus, http.StatusInternalServerError) + } else if tt.headerEvent == "" { + assert.Equal(t, tt.expectedStatus, http.StatusBadRequest) + } else { + assert.Equal(t, tt.expectedStatus, w.Code) + } + }) + } +} +func TestPostGitlab(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a new mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock NATSContext + mockConn := mocks.NewMockNATSClientInterface(ctrl) + // Create an instance of the Application struct + app := &Application{conn: mockConn} + + // Define the test cases + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Success", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `GITLAB DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Missing Event Header", + headerEvent: "", + bodyData: nil, + expectedLog: "error getting the gitlab event from header", + expectedStatus: http.StatusBadRequest, + mockPublishErr: nil, + }, + { + name: "Publish Error", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `GITLAB DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("publish error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup the mock response + if tt.headerEvent != "" { + mockConn.EXPECT().Publish(tt.bodyData, string(model.GitlabProvider), model.GitlabHeader, model.EventValue(tt.headerEvent)).Return(tt.mockPublishErr).Times(1) + } + + // Create a new Gin context with the necessary headers and body + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + c.Request.Header.Set(string(model.GitlabHeader), tt.headerEvent) + + // Capture logs + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + // Call the PostGitlab method + app.PostGitlab(c) + + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Verify the response status code + if tt.mockPublishErr != nil { + assert.Equal(t, tt.expectedStatus, http.StatusInternalServerError) + } else if tt.headerEvent == "" { + assert.Equal(t, tt.expectedStatus, http.StatusBadRequest) + } else { + assert.Equal(t, tt.expectedStatus, w.Code) + } + }) + } +} + +func TestPostBitbucket(t *testing.T) { + // Set Gin to test mode + gin.SetMode(gin.TestMode) + + // Create a new mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock NATSContext + mockConn := mocks.NewMockNATSClientInterface(ctrl) + // Create an instance of the Application struct + app := &Application{conn: mockConn} + + // Define the test cases + tests := []struct { + name string + headerEvent string + bodyData []byte + expectedLog string + expectedStatus int + mockPublishErr error + }{ + { + name: "Success", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `BITBUCKET DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusOK, + mockPublishErr: nil, + }, + { + name: "Missing Event Header", + headerEvent: "", + bodyData: nil, + expectedLog: "error getting the bitbucket event from header", + expectedStatus: http.StatusBadRequest, + mockPublishErr: nil, + }, + { + name: "Publish Error", + headerEvent: "push", + bodyData: []byte(`{"ref": "refs/heads/main"}`), + expectedLog: `BITBUCKET DATA: "{\"ref\": \"refs/heads/main\"}"`, + expectedStatus: http.StatusInternalServerError, + mockPublishErr: errors.New("publish error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup the mock response + if tt.headerEvent != "" { + mockConn.EXPECT().Publish(tt.bodyData, string(model.BitBucketProvider), model.BitBucketHeader, model.EventValue(tt.headerEvent)).Return(tt.mockPublishErr).Times(1) + } + + // Create a new Gin context with the necessary headers and body + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.bodyData)) + c.Request.Header.Set(string(model.BitBucketHeader), tt.headerEvent) + + // Capture logs + var logOutput bytes.Buffer + log.SetOutput(&logOutput) + defer log.SetOutput(os.Stderr) + + // Call the PostBitbucket method + app.PostBitbucket(c) + + // Verify the log output using strings.Contains + logStr := logOutput.String() + assert.Contains(t, logStr, tt.expectedLog, "log output should contain the expected log") + + // Verify the response status code + if tt.mockPublishErr != nil { + assert.Equal(t, tt.expectedStatus, http.StatusInternalServerError) + } else if tt.headerEvent == "" { + assert.Equal(t, tt.expectedStatus, http.StatusBadRequest) + } else { + assert.Equal(t, tt.expectedStatus, w.Code) + } + }) + } +} +func TestClose(t *testing.T) { + // Create a new mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create a mock NATSContext + mockConn := mocks.NewMockNATSClientInterface(ctrl) // Expect the Close method to be called + mockConn.EXPECT().Close().Times(1) + + // Create a mock http.Server + mockServer := &http.Server{} + // Expect the Shutdown method to be called + //mockServer.EXPECT().Shutdown(gomock.Any()).Return(nil).Times(1) + + // Create an instance of the Application struct + app := &Application{ + conn: mockConn, + server: mockServer, + } + + // Call the Close method + app.Close() + + // Verify that the expectations were met + // This is optional depending on your needs + // You can use assert from the testify package or similar +} diff --git a/agent/git/pkg/clients/mocks/nats_client_mock.go b/agent/git/pkg/clients/mocks/nats_client_mock.go new file mode 100644 index 00000000..75327e10 --- /dev/null +++ b/agent/git/pkg/clients/mocks/nats_client_mock.go @@ -0,0 +1,77 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: nats_client.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + model "github.com/intelops/kubviz/model" + nats "github.com/nats-io/nats.go" +) + +// MockNATSClientInterface is a mock of NATSClientInterface interface. +type MockNATSClientInterface struct { + ctrl *gomock.Controller + recorder *MockNATSClientInterfaceMockRecorder +} + +// MockNATSClientInterfaceMockRecorder is the mock recorder for MockNATSClientInterface. +type MockNATSClientInterfaceMockRecorder struct { + mock *MockNATSClientInterface +} + +// NewMockNATSClientInterface creates a new mock instance. +func NewMockNATSClientInterface(ctrl *gomock.Controller) *MockNATSClientInterface { + mock := &MockNATSClientInterface{ctrl: ctrl} + mock.recorder = &MockNATSClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNATSClientInterface) EXPECT() *MockNATSClientInterfaceMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockNATSClientInterface) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockNATSClientInterfaceMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockNATSClientInterface)(nil).Close)) +} + +// CreateStream mocks base method. +func (m *MockNATSClientInterface) CreateStream() (nats.JetStreamContext, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateStream") + ret0, _ := ret[0].(nats.JetStreamContext) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateStream indicates an expected call of CreateStream. +func (mr *MockNATSClientInterfaceMockRecorder) CreateStream() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStream", reflect.TypeOf((*MockNATSClientInterface)(nil).CreateStream)) +} + +// Publish mocks base method. +func (m *MockNATSClientInterface) Publish(metric []byte, repo string, eventkey model.EventKey, eventvalue model.EventValue) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Publish", metric, repo, eventkey, eventvalue) + ret0, _ := ret[0].(error) + return ret0 +} + +// Publish indicates an expected call of Publish. +func (mr *MockNATSClientInterfaceMockRecorder) Publish(metric, repo, eventkey, eventvalue interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockNATSClientInterface)(nil).Publish), metric, repo, eventkey, eventvalue) +} diff --git a/agent/git/pkg/clients/nats_client.go b/agent/git/pkg/clients/nats_client.go index 7d6830b4..1f086d51 100644 --- a/agent/git/pkg/clients/nats_client.go +++ b/agent/git/pkg/clients/nats_client.go @@ -17,6 +17,12 @@ import ( "github.com/nats-io/nats.go" ) +type NATSClientInterface interface { + Close() + CreateStream() (nats.JetStreamContext, error) + Publish(metric []byte, repo string, eventkey model.EventKey, eventvalue model.EventValue) error +} + // constant variables to use with nats stream and // nats publishing const (