From 9124c7a71bf6039f2144e03ea2fa5d60371a88f9 Mon Sep 17 00:00:00 2001 From: jeff Date: Mon, 9 Dec 2024 16:50:02 +0800 Subject: [PATCH] feat: Add new API notebook --- internal/context/context.go | 4 + internal/sbi/api_notebook.go | 110 +++++++++++++++++++++ internal/sbi/api_notebook_test.go | 102 +++++++++++++++++++ internal/sbi/processor/notebook.go | 58 +++++++++++ internal/sbi/processor/notebook_test.go | 125 ++++++++++++++++++++++++ internal/sbi/router.go | 3 + 6 files changed, 402 insertions(+) create mode 100644 internal/sbi/api_notebook.go create mode 100644 internal/sbi/api_notebook_test.go create mode 100644 internal/sbi/processor/notebook.go create mode 100644 internal/sbi/processor/notebook_test.go diff --git a/internal/context/context.go b/internal/context/context.go index 3b98168..4649273 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -18,6 +18,7 @@ type NFContext struct { SBIPort int SpyFamilyData map[string]string + NoteData map[string]string } var nfContext = NFContext{} @@ -57,6 +58,9 @@ func InitNfContext() { "Henry": "Henderson", "Martha": "Marriott", } + nfContext.NoteData = map[string]string{ + "User_Guide": "// Show the content of the note with title <Title>.\n", + } } func GetSelf() *NFContext { diff --git a/internal/sbi/api_notebook.go b/internal/sbi/api_notebook.go new file mode 100644 index 0000000..c0c5491 --- /dev/null +++ b/internal/sbi/api_notebook.go @@ -0,0 +1,110 @@ +package sbi + +import ( + "net/http" + + "github.com/andy89923/nf-example/internal/logger" + "github.com/gin-gonic/gin" +) + +func (s *Server) getNotebookRoute() []Route { + return []Route{ + { + Name: "Show Note", + Method: http.MethodGet, + Pattern: "/:Title", + APIFunc: s.HTTPShowNote, + // Use + // curl -X GET http://127.0.0.163:8000/notebook/User_Guide -w "\n" + // "Title: User_Guide" + // " /<Title>/ Show the content of the note with title <Title>." + }, + { + Name: "Create Note", + Method: http.MethodPut, + Pattern: "/:Title/:Content", + APIFunc: s.HTTPCreateNote, + // Use + // curl -X PUT http://127.0.0.163:8000/notebook/New_Title/New_content -w "\n" + // "Title: New_Title" + // "Content:" + // " New_content" + }, + { + Name: "Update Note", + Method: http.MethodPost, + Pattern: "/:Title/:Content", + APIFunc: s.HTTPUpdateNote, + // Use + // curl -X POST http://127.0.0.163:8000/notebook/New_Title/New_content -w "\n" + // "Title: New_Title" + // "Content:" + // " New_content" + }, + { + Name: "Note Append with whitespace at front.", + Method: http.MethodPost, + Pattern: "/:Title/append/:Content_append", + APIFunc: s.HTTPNoteWhitespaceAppend, + // Use + // curl -X POST http://127.0.0.163:8000/notebook/New_Title/append/new_content_to_be_append. -w "\n" + // "Title: New_Title" + // "Content:" + // " New_content new_content_to_be_append." + }, + } +} + +func (s *Server) HTTPShowNote(c *gin.Context) { + logger.SBILog.Infof("In HTTPShowNote") + + targetName := c.Param("Title") + if targetName == "" { + c.String(http.StatusBadRequest, "No name provided") + return + } + + s.Processor().FindNote(c, targetName) +} + +func (s *Server) HTTPUpdateNote(c *gin.Context) { + logger.SBILog.Infof("In HTTPUpdateNote") + + targetName := c.Param("Title") + if targetName == "" { + c.String(http.StatusBadRequest, "No name provided") + return + } + + newContent := c.Param("Content") + + s.Processor().UpdateNote(c, targetName, newContent) +} + +func (s *Server) HTTPCreateNote(c *gin.Context) { + logger.SBILog.Infof("In HTTPCreateNote") + + targetName := c.Param("Title") + if targetName == "" { + c.String(http.StatusBadRequest, "No name provided") + return + } + + newContent := c.Param("Content") + + s.Processor().CreateNote(c, targetName, newContent) +} + +func (s *Server) HTTPNoteWhitespaceAppend(c *gin.Context) { + logger.SBILog.Infof("In HTTPNoteAppend") + + targetName := c.Param("Title") + if targetName == "" { + c.String(http.StatusBadRequest, "No name provided") + return + } + + newContent := c.Param("Content_append") + + s.Processor().NoteWhitespaceAppend(c, targetName, newContent) +} diff --git a/internal/sbi/api_notebook_test.go b/internal/sbi/api_notebook_test.go new file mode 100644 index 0000000..ec53531 --- /dev/null +++ b/internal/sbi/api_notebook_test.go @@ -0,0 +1,102 @@ +package sbi_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/andy89923/nf-example/internal/sbi" + "github.com/andy89923/nf-example/pkg/factory" + "github.com/gin-gonic/gin" + "go.uber.org/mock/gomock" +) + +func Test_getNotebookRoutes(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("No name provided", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "No name provided" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + var err error + ginCtx.Request, err = http.NewRequest("GET", "/notebook/", nil) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + + server.HTTPShowNote(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()) + } + }) + + t.Run("No name provided", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "No name provided" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + var err error + ginCtx.Request, err = http.NewRequest("POST", "/notebook/", nil) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + + server.HTTPUpdateNote(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()) + } + }) + + t.Run("No name provided", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "No name provided" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + var err error + ginCtx.Request, err = http.NewRequest("POST", "/notebook//append/", nil) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + + server.HTTPUpdateNote(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/notebook.go b/internal/sbi/processor/notebook.go new file mode 100644 index 0000000..8c8e34f --- /dev/null +++ b/internal/sbi/processor/notebook.go @@ -0,0 +1,58 @@ +package processor + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func (p *Processor) FindNote(c *gin.Context, targetName string) { + if content, ok := p.Context().NoteData[targetName]; ok { + c.String(http.StatusOK, fmt.Sprintf("Title: %s\nContent:\n\t%s\n", targetName, content)) + return + } + c.String(http.StatusNotFound, fmt.Sprintf("[%s] not found in Notebook\n", targetName)) +} + +func (p *Processor) UpdateNote(c *gin.Context, targetName string, newContent string) { + p.Context().NoteData[targetName] = newContent + + if content, ok := p.Context().NoteData[targetName]; ok { + c.String(http.StatusOK, fmt.Sprintf("Title: %s\nContent:\n\t%s\n", targetName, content)) + return + } + c.String(http.StatusNotFound, fmt.Sprintf("[%s] not found in Notebook\n", targetName)) +} + +func (p *Processor) CreateNote(c *gin.Context, targetName string, newContent string) { + if _, ok := p.Context().NoteData[targetName]; !ok { + p.Context().NoteData[targetName] = newContent + + content := p.Context().NoteData[targetName] + c.String(http.StatusOK, fmt.Sprintf("Title: %s\nContent:\n\t%s\n", targetName, content)) + return + } else if ok { + c.String(http.StatusForbidden, fmt.Sprintf( + "[%s] already exist. Please use POST to modify the content.\n", + targetName), + ) + } +} + +func (p *Processor) NoteWhitespaceAppend(c *gin.Context, targetName string, newContent string) { + if _, ok := p.Context().NoteData[targetName]; ok { + var sb strings.Builder + sb.WriteString(p.Context().NoteData[targetName]) + sb.WriteString(" ") + sb.WriteString(newContent) + + p.Context().NoteData[targetName] = sb.String() + + content := p.Context().NoteData[targetName] + c.String(http.StatusOK, fmt.Sprintf("Title: %s\nContent:\n\t%s\n", targetName, content)) + return + } + c.String(http.StatusNotFound, fmt.Sprintf("[%s] not found in Notebook\n", targetName)) +} diff --git a/internal/sbi/processor/notebook_test.go b/internal/sbi/processor/notebook_test.go new file mode 100644 index 0000000..a9e9996 --- /dev/null +++ b/internal/sbi/processor/notebook_test.go @@ -0,0 +1,125 @@ +package processor_test + +import ( + "net/http/httptest" + "testing" + + nf_context "github.com/andy89923/nf-example/internal/context" + "github.com/andy89923/nf-example/internal/sbi/processor" + "github.com/gin-gonic/gin" + gomock "go.uber.org/mock/gomock" +) + +func Test_Note(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("Find and Show Note", func(t *testing.T) { + const INPUT_NAME = "User_Guide" + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "Title: " + INPUT_NAME + + "\nContent:\n\t" + + "/<Title>/ Show the content of the note with title <Title>.\n\n" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + NoteData: map[string]string{ + "User_Guide": "/<Title>/ Show the content of the note with title <Title>.\n", + }, + }) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.FindNote(ginCtx, INPUT_NAME) + + 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()) + } + }) + + t.Run("Find Note That Does Not Exist", func(t *testing.T) { + const INPUT_NAME = "Andy" + const EXPECTED_STATUS = 404 + const EXPECTED_BODY = "[" + INPUT_NAME + "] not found in Notebook\n" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + NoteData: map[string]string{ + "User_Guide": "/<Title>/ Show the content of the note with title <Title>.\n", + }, + }) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.FindNote(ginCtx, INPUT_NAME) + + 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()) + } + }) + + t.Run("Update Note", func(t *testing.T) { + const INPUT_NAME = "new_note_title" + const INPUT_CONTENT = "Content" + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "Title: " + INPUT_NAME + + "\nContent:\n\t" + INPUT_CONTENT + "\n" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + NoteData: map[string]string{ + "User_Guide": "/<Title>/ Show the content of the note with title <Title>.\n", + }, + }).AnyTimes() + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.UpdateNote(ginCtx, INPUT_NAME, INPUT_CONTENT) + + 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()) + } + }) + + t.Run("Append with Whitespace Prefix on Note", func(t *testing.T) { + const INPUT_NAME = "new_note_title" + const INPUT_CONTENT = "can't_contain_whitespace." + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "Title: " + INPUT_NAME + + "\nContent:\n\tContent " + INPUT_CONTENT + "\n" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + NoteData: map[string]string{ + "User_Guide": "/<Title>/ Show the content of the note with title <Title>.\n", + }, + }).AnyTimes() + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + processor.NoteWhitespaceAppend(ginCtx, INPUT_NAME, INPUT_CONTENT) + + 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/router.go b/internal/sbi/router.go index 1d16794..3da7a84 100644 --- a/internal/sbi/router.go +++ b/internal/sbi/router.go @@ -63,6 +63,9 @@ func newRouter(s *Server) *gin.Engine { helloGroup := router.Group("/hello") applyRoutes(helloGroup, s.getHelloRoute()) + notebookGroup := router.Group("/notebook") + applyRoutes(notebookGroup, s.getNotebookRoute()) + return router }