diff --git a/.golangci.yml b/.golangci.yml index 2f97f11..2a050fb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -53,7 +53,14 @@ linters: goconst: min-len: 5 min-occurrences: 3 - ignore-strings-values: get|post|put|delete|patch|options|head + ignore-string-values: + - get + - post + - put + - delete + - patch + - options + - head gocritic: disabled-checks: - regexpMust diff --git a/go.mod b/go.mod index b773f7f..ff87899 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.15 go.uber.org/mock v0.4.0 gopkg.in/yaml.v2 v2.4.0 @@ -20,6 +21,7 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -35,6 +37,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/internal/context/context.go b/internal/context/context.go index 2bfe8b2..0f5999b 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -2,6 +2,7 @@ package context import ( "os" + "sync" "github.com/Alonza0314/nf-example/internal/logger" "github.com/Alonza0314/nf-example/pkg/factory" @@ -10,6 +11,11 @@ import ( "github.com/free5gc/openapi/models" ) +type Task struct { + ID int `json:"id"` + Name string `json:"name"` +} + type NFContext struct { NfId string Name string @@ -18,6 +24,13 @@ type NFContext struct { SBIPort int SpyFamilyData map[string]string + + MessageRecord []string + MessageMu sync.Mutex + + Tasks []Task + TaskMutex sync.RWMutex + NextTaskID uint64 } var nfContext = NFContext{} @@ -57,6 +70,11 @@ func InitNfContext() { "Henry": "Henderson", "Martha": "Marriott", } + + nfContext.MessageRecord = []string{} + + nfContext.Tasks = make([]Task, 0) + nfContext.NextTaskID = 0 } func GetSelf() *NFContext { diff --git a/internal/sbi/api_message.go b/internal/sbi/api_message.go new file mode 100644 index 0000000..02ba3e9 --- /dev/null +++ b/internal/sbi/api_message.go @@ -0,0 +1,54 @@ +package sbi + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func (s *Server) myPutGetMessageRoute() []Route { + return []Route{ + { + Name: "get messages", + Method: http.MethodGet, + Pattern: "/", + APIFunc: s.HTTPGetMessageRecord, + // Use + // curl -X GET http://127.0.0.163:8000/message/ -w "\n" + // return all added message + }, + { + Name: "add message", + Method: http.MethodPut, + Pattern: "/:Message", + APIFunc: s.HTTPAddNewMessage, + // Use + // curl -X PUT http://127.0.0.163:8000/message/yourmessage -w "\n" + // add "yourmessage" to message record + }, + { + // empty input handle, will not accept + Name: "empty input", + Method: http.MethodPut, + Pattern: "/", + APIFunc: s.noMessageHandler, + }, + } +} + +func (s *Server) HTTPAddNewMessage(c *gin.Context) { + newMessage := c.Param("Message") + if newMessage == "" { + s.noMessageHandler(c) + return + } + s.Processor().AddNewMessage(c, newMessage) +} + +func (s *Server) noMessageHandler(c *gin.Context) { + c.String(http.StatusBadRequest, "No message provided") +} + +func (s *Server) HTTPGetMessageRecord(c *gin.Context) { + s.Processor().GetMessageRecord(c) +} diff --git a/internal/sbi/api_message_test.go b/internal/sbi/api_message_test.go new file mode 100644 index 0000000..cf6adaa --- /dev/null +++ b/internal/sbi/api_message_test.go @@ -0,0 +1,52 @@ +package sbi_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Alonza0314/nf-example/internal/sbi" + "github.com/Alonza0314/nf-example/pkg/factory" + "github.com/gin-gonic/gin" + "go.uber.org/mock/gomock" +) + +func Test_PUTWithEmptyInput(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockCtrl := gomock.NewController(t) + nfApp := sbi.NewMocknfApp(mockCtrl) + nfApp.EXPECT().Config().Return(&factory.Config{ + Configuration: &factory.Configuration{ + Sbi: &factory.Sbi{ + Port: 8000, + }, + }, + }).AnyTimes() + server := sbi.NewServer(nfApp, "") + + t.Run("Add Message That Empty", func(t *testing.T) { + const EXPECTED_STATUS = 400 + const EXPECTED_BODY = "No message provided" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + var err error + ginCtx.Request, err = http.NewRequest("PUT", "/message/", nil) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + + server.HTTPAddNewMessage(ginCtx) + + if httpRecorder.Code != EXPECTED_STATUS { + t.Errorf("Expected status code %d, got %d", EXPECTED_STATUS, httpRecorder.Code) + } + + if httpRecorder.Body.String() != EXPECTED_BODY { + t.Errorf("Expected body %s, got %s", EXPECTED_BODY, httpRecorder.Body.String()) + } + }) +} diff --git a/internal/sbi/api_task.go b/internal/sbi/api_task.go new file mode 100644 index 0000000..8733e31 --- /dev/null +++ b/internal/sbi/api_task.go @@ -0,0 +1,32 @@ +package sbi + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func (s *Server) HTTPCreateNewTask(c *gin.Context) { + s.Processor().CreateNewTask(c) +} + +func (s *Server) HTTPGetAllTasks(c *gin.Context) { + s.Processor().GetAllTasks(c) +} + +func (s *Server) getTaskRoute() []Route { + return []Route{ + { + Name: "Get All Tasks", + Method: http.MethodGet, + Pattern: "/tasks", + APIFunc: s.HTTPGetAllTasks, + }, + { + Name: "Create New Task", + Method: http.MethodPost, + Pattern: "/tasks", + APIFunc: s.HTTPCreateNewTask, + }, + } +} diff --git a/internal/sbi/processor/message.go b/internal/sbi/processor/message.go new file mode 100644 index 0000000..b2154ff --- /dev/null +++ b/internal/sbi/processor/message.go @@ -0,0 +1,33 @@ +package processor + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +func (p *Processor) AddNewMessage(c *gin.Context, newMessage string) { + p.Context().MessageMu.Lock() + defer p.Context().MessageMu.Unlock() + // add message + p.Context().MessageRecord = append(p.Context().MessageRecord, newMessage) + c.String(http.StatusOK, "add a new message!") +} + +func (p *Processor) GetMessageRecord(c *gin.Context) { + p.Context().MessageMu.Lock() + defer p.Context().MessageMu.Unlock() + + // no content + if len(p.Context().MessageRecord) == 0 { + c.String(http.StatusOK, "no message now, add some messagess!") + return + } + // get record + Record := "" + for _, s := range p.Context().MessageRecord { + Record += fmt.Sprintf("%s\n", s) + } + c.String(http.StatusOK, Record) +} diff --git a/internal/sbi/processor/message_test.go b/internal/sbi/processor/message_test.go new file mode 100644 index 0000000..f970891 --- /dev/null +++ b/internal/sbi/processor/message_test.go @@ -0,0 +1,113 @@ +package processor_test + +import ( + "net/http/httptest" + "testing" + + nf_context "github.com/Alonza0314/nf-example/internal/context" + "github.com/Alonza0314/nf-example/internal/sbi/processor" + "github.com/gin-gonic/gin" + gomock "go.uber.org/mock/gomock" +) + +func Test_AddNewMessage(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockCtrl := gomock.NewController(t) + processorNf := processor.NewMockProcessorNf(mockCtrl) + processor, err := processor.NewProcessor(processorNf) + if err != nil { + t.Errorf("Failed to create processor: %s", err) + return + } + + t.Run("Add Message That Not Empty", func(t *testing.T) { + const INPUT_MESSAGE = "ABC" + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "add a new message!" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + MessageRecord: []string{}, + }).AnyTimes() + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.AddNewMessage(ginCtx, INPUT_MESSAGE) + + if httpRecorder.Code != EXPECTED_STATUS { + t.Errorf("Expected status code %d, got %d", EXPECTED_STATUS, httpRecorder.Code) + } + + if httpRecorder.Body.String() != EXPECTED_BODY { + t.Errorf("Expected body %s, got %s", EXPECTED_BODY, httpRecorder.Body.String()) + } + }) +} + +func Test_GetMessageNotEmpty(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockCtrl := gomock.NewController(t) + processorNf := processor.NewMockProcessorNf(mockCtrl) + processor, err := processor.NewProcessor(processorNf) + if err != nil { + t.Errorf("Failed to create processor: %s", err) + return + } + + t.Run("Get Message That Not Empty", func(t *testing.T) { + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "ABC\n123\n" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + MessageRecord: []string{ + "ABC", + "123", + }, + }).AnyTimes() + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.GetMessageRecord(ginCtx) + + if httpRecorder.Code != EXPECTED_STATUS { + t.Errorf("Expected status code %d, got %d", EXPECTED_STATUS, httpRecorder.Code) + } + + if httpRecorder.Body.String() != EXPECTED_BODY { + t.Errorf("Expected body %s, got %s", EXPECTED_BODY, httpRecorder.Body.String()) + } + }) +} + +func Test_GetMessageEmpty(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockCtrl := gomock.NewController(t) + processorNf := processor.NewMockProcessorNf(mockCtrl) + processor, err := processor.NewProcessor(processorNf) + if err != nil { + t.Errorf("Failed to create processor: %s", err) + return + } + t.Run("Get Message That Empty", func(t *testing.T) { + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "no message now, add some messagess!" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + MessageRecord: []string{}, + }).AnyTimes() + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.GetMessageRecord(ginCtx) + + if httpRecorder.Code != EXPECTED_STATUS { + t.Errorf("Expected status code %d, got %d", EXPECTED_STATUS, httpRecorder.Code) + } + + if httpRecorder.Body.String() != EXPECTED_BODY { + t.Errorf("Expected body %s, got %s", EXPECTED_BODY, httpRecorder.Body.String()) + } + }) +} diff --git a/internal/sbi/processor/task_handler.go b/internal/sbi/processor/task_handler.go new file mode 100644 index 0000000..19fa9bc --- /dev/null +++ b/internal/sbi/processor/task_handler.go @@ -0,0 +1,35 @@ +package processor + +import ( + "net/http" + "sync/atomic" + + "github.com/Alonza0314/nf-example/internal/context" + "github.com/gin-gonic/gin" +) + +func (p *Processor) CreateNewTask(c *gin.Context) { + var newTask context.Task + if err := c.ShouldBindJSON(&newTask); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) + return + } + + // next ID + newID := atomic.AddUint64(&p.Context().NextTaskID, 1) + newTask.ID = int(newID) + + p.Context().TaskMutex.Lock() + p.Context().Tasks = append(p.Context().Tasks, newTask) + p.Context().TaskMutex.Unlock() + + c.JSON(http.StatusCreated, newTask) +} + +func (p *Processor) GetAllTasks(c *gin.Context) { + p.Context().TaskMutex.RLock() + tasksCopy := make([]context.Task, len(p.Context().Tasks)) + copy(tasksCopy, p.Context().Tasks) + p.Context().TaskMutex.RUnlock() + c.JSON(http.StatusOK, tasksCopy) +} diff --git a/internal/sbi/processor/task_handler_test.go b/internal/sbi/processor/task_handler_test.go new file mode 100644 index 0000000..93af29b --- /dev/null +++ b/internal/sbi/processor/task_handler_test.go @@ -0,0 +1,78 @@ +package processor_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/Alonza0314/nf-example/internal/context" + "github.com/Alonza0314/nf-example/internal/sbi/processor" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func Test_TaskHandlers(t *testing.T) { + gin.SetMode(gin.TestMode) + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockNfApp := processor.NewMockProcessorNf(mockCtrl) + + nfContext := &context.NFContext{ + Tasks: make([]context.Task, 0), + TaskMutex: sync.RWMutex{}, + NextTaskID: 0, + } + mockNfApp.EXPECT().Context().Return(nfContext).AnyTimes() + + proc, err := processor.NewProcessor(mockNfApp) + assert.NoError(t, err) + + t.Run("Create Task", func(t *testing.T) { + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + taskData := map[string]string{"name": "Test Task"} + var body []byte + body, err = json.Marshal(taskData) + assert.NoError(t, err) + + ginCtx.Request, err = http.NewRequest(http.MethodPost, "/task/tasks", bytes.NewReader(body)) + assert.NoError(t, err) + + ginCtx.Request.Header.Set("Content-Type", "application/json") + + proc.CreateNewTask(ginCtx) + + assert.Equal(t, http.StatusCreated, httpRecorder.Code) + + var createdTask context.Task + err = json.Unmarshal(httpRecorder.Body.Bytes(), &createdTask) + assert.NoError(t, err) + assert.Equal(t, "Test Task", createdTask.Name) + assert.Equal(t, 1, createdTask.ID) + }) + + t.Run("Get All Tasks", func(t *testing.T) { + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + ginCtx.Request, err = http.NewRequest(http.MethodGet, "/task/tasks", nil) + assert.NoError(t, err) + + proc.GetAllTasks(ginCtx) + + assert.Equal(t, http.StatusOK, httpRecorder.Code) + + var tasks []context.Task + err = json.Unmarshal(httpRecorder.Body.Bytes(), &tasks) + assert.NoError(t, err) + assert.Len(t, tasks, 1) + assert.Equal(t, "Test Task", tasks[0].Name) + }) +} diff --git a/internal/sbi/router.go b/internal/sbi/router.go index b62139f..97703dc 100644 --- a/internal/sbi/router.go +++ b/internal/sbi/router.go @@ -42,9 +42,15 @@ func newRouter(s *Server) *gin.Engine { defaultGroup := router.Group("/default") applyRoutes(defaultGroup, s.getDefaultRoute()) + myPutGetMessageGroup := router.Group("/message") + applyRoutes(myPutGetMessageGroup, s.myPutGetMessageRoute()) + spyFamilyGroup := router.Group("/spyfamily") applyRoutes(spyFamilyGroup, s.getSpyFamilyRoute()) + taskGroup := router.Group("/task") + applyRoutes(taskGroup, s.getTaskRoute()) + return router }