diff --git a/internal/context/context.go b/internal/context/context.go index 3d3b928..0d1e5cd 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -37,6 +37,8 @@ type NFContext struct { Fortunes []string FortuneMutex sync.RWMutex + + TimeZoneData map[string]string } type Message struct { @@ -110,6 +112,19 @@ func InitNfContext() { "凶: Be careful, misfortune may be ahead.", "大凶: A great misfortune is coming. Be prepared.", } + + nfContext.TimeZoneData = map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + "Seoul": "UTC+9", + "NewYork": "UTC-5", + "Paris": "UTC+2", + "London": "UTC+1", + "Berlin": "UTC+2", + "Sydney": "UTC+10", + "Moscow": "UTC+3", + "Dubai": "UTC+4", + } } func GetSelf() *NFContext { diff --git a/internal/sbi/api_timezone.go b/internal/sbi/api_timezone.go new file mode 100644 index 0000000..a19d600 --- /dev/null +++ b/internal/sbi/api_timezone.go @@ -0,0 +1,119 @@ +package sbi + +import ( + "net/http" + + "github.com/Alonza0314/nf-example/internal/logger" + "github.com/Alonza0314/nf-example/internal/sbi/processor" + "github.com/gin-gonic/gin" +) + +func (s *Server) getTimeZoneRoute() []Route { + return []Route{ + { + Name: "Welcome to timezone service", + Method: http.MethodGet, + Pattern: "/", + APIFunc: func(c *gin.Context) { + c.String(http.StatusOK, "Welcome to time zone query service") + }, + // Use + // curl -X GET http://127.0.0.163:8000/timezone/ -w "\n" + }, + { + Name: "Query city time zone", + Method: http.MethodGet, + Pattern: "/city/:City", + APIFunc: s.HTTPGetTimeZoneByCity, + // Use + // curl -X GET http://127.0.0.163:8000/timezone/city/Taipei -w "\n" + }, + { + Name: "Add new city time zone", + Method: http.MethodPost, + Pattern: "/city", + APIFunc: s.HTTPAddNewCityTimeZone, + // Use + // curl -X POST http://127.0.0.163:8000/timezone/city -d '{"City": "Chicago", "TimeZone": "UTC-5"}' -w "\n" + }, + { + Name: "Reset city time zone", + Method: http.MethodPost, + Pattern: "/city/:City", + APIFunc: s.HTTPResetCityTimeZone, + // Use + // curl -X POST http://127.0.0.163:8000/timezone/city/Chicago -d '{"TimeZone": "UTC-6"}' -w "\n" + }, + { + Name: "Delete city time zone", + Method: http.MethodDelete, + Pattern: "/city/:City", + APIFunc: s.HTTPDeleteCityTimeZone, + // Usage: + // curl -X DELETE http://127.0.0.163:8000/timezone/city/Chicago -w "\n" + }, + } +} + +func (s *Server) HTTPGetTimeZoneByCity(c *gin.Context) { + logger.SBILog.Infof("In HTTPGetTimeZoneByCity") + + city := c.Param("City") + if city == "" { + c.String(http.StatusBadRequest, "No city provided") + return + } + s.Processor().HandleGetTimeZone(c, city) +} + +func (s *Server) HTTPAddNewCityTimeZone(c *gin.Context) { + logger.SBILog.Infof("In HTTPAddNewCityTimeZone") + + var req processor.TimeZoneRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.String(http.StatusBadRequest, "Invalid JSON") + return + } + + if req.City == "" || req.TimeZone == "" { + c.String(http.StatusBadRequest, "City and TimeZone fields are required") + return + } + s.Processor().HandleAddNewCityTimeZone(c, req) +} + +func (s *Server) HTTPResetCityTimeZone(c *gin.Context) { + logger.SBILog.Infof("In HTTPResetCityTimeZone") + + city := c.Param("City") + if city == "" { + c.String(http.StatusBadRequest, "No city provided") + return + } + + var req struct { + TZ string `json:"TimeZone"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.String(http.StatusBadRequest, "Invalid JSON format, expected object with TimeZone field") + return + } + + if req.TZ == "" { + c.String(http.StatusBadRequest, "TimeZone field is required") + return + } + + s.Processor().HandleResetCityTimeZone(c, city, req.TZ) +} + +func (s *Server) HTTPDeleteCityTimeZone(c *gin.Context) { + logger.SBILog.Infof("In HTTPDeleteCityTimeZone") + + city := c.Param("City") + if city == "" { + c.String(http.StatusBadRequest, "No city provided") + return + } + s.Processor().HandleDeleteCityTimeZone(c, city) +} diff --git a/internal/sbi/api_timezone_test.go b/internal/sbi/api_timezone_test.go new file mode 100644 index 0000000..fa52db9 --- /dev/null +++ b/internal/sbi/api_timezone_test.go @@ -0,0 +1,243 @@ +package sbi_test + +import ( + "bytes" + "io" + "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 setupTimeZoneTestServer(t *testing.T) *sbi.Server { + 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() + return sbi.NewServer(nfApp, "") +} + +func createJSONRequest( + t *testing.T, + method, url string, + jsonBody string, + params gin.Params, +) (*httptest.ResponseRecorder, *gin.Context) { + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + if params != nil { + ginCtx.Params = params + } + + var body io.Reader + if jsonBody != "" { + body = bytes.NewBufferString(jsonBody) + } + + var err error + ginCtx.Request, err = http.NewRequest(method, url, body) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return nil, nil + } + if jsonBody != "" { + ginCtx.Request.Header.Set("Content-Type", "application/json") + } + + return httpRecorder, ginCtx +} + +func Test_HTTPGetTimeZoneByCity(t *testing.T) { + server := setupTimeZoneTestServer(t) + + t.Run("No city provided", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "No city provided" + + httpRecorder, ginCtx := createJSONRequest(t, "GET", "/timezone/city/", "", nil) + if ginCtx == nil { + return + } + + server.HTTPGetTimeZoneByCity(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_HTTPAddNewCityTimeZone(t *testing.T) { + server := setupTimeZoneTestServer(t) + + t.Run("Invalid JSON", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "Invalid JSON" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + invalidJSON := bytes.NewBufferString("{invalid json}") + var err error + ginCtx.Request, err = http.NewRequest("POST", "/timezone/city", invalidJSON) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + ginCtx.Request.Header.Set("Content-Type", "application/json") + + server.HTTPAddNewCityTimeZone(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("Missing required fields", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "City and TimeZone fields are required" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + incompleteJSON := bytes.NewBufferString(`{"City": "", "TimeZone": "UTC+8"}`) + var err error + ginCtx.Request, err = http.NewRequest("POST", "/timezone/city", incompleteJSON) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + ginCtx.Request.Header.Set("Content-Type", "application/json") + + server.HTTPAddNewCityTimeZone(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_HTTPResetCityTimeZone(t *testing.T) { + server := setupTimeZoneTestServer(t) + + t.Run("No city provided", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "No city provided" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + validJSON := bytes.NewBufferString(`{"TimeZone": "UTC+8"}`) + var err error + ginCtx.Request, err = http.NewRequest("POST", "/timezone/city/", validJSON) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + ginCtx.Request.Header.Set("Content-Type", "application/json") + + server.HTTPResetCityTimeZone(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("Invalid JSON format", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "Invalid JSON format, expected object with TimeZone field" + + httpRecorder, ginCtx := createJSONRequest(t, "POST", "/timezone/city/Taipei", + "{invalid json}", gin.Params{gin.Param{Key: "City", Value: "Taipei"}}) + if ginCtx == nil { + return + } + + server.HTTPResetCityTimeZone(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("Missing TimeZone field", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "TimeZone field is required" + + httpRecorder, ginCtx := createJSONRequest(t, "POST", "/timezone/city/Taipei", + `{"TimeZone": ""}`, gin.Params{gin.Param{Key: "City", Value: "Taipei"}}) + if ginCtx == nil { + return + } + + server.HTTPResetCityTimeZone(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_HTTPDeleteCityTimeZone(t *testing.T) { + server := setupTimeZoneTestServer(t) + + t.Run("No city provided", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "No city provided" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + var err error + ginCtx.Request, err = http.NewRequest("DELETE", "/timezone/city/", nil) + if err != nil { + t.Errorf("Failed to create request: %s", err) + return + } + + server.HTTPDeleteCityTimeZone(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/time_zone.go b/internal/sbi/processor/time_zone.go new file mode 100644 index 0000000..d3ca302 --- /dev/null +++ b/internal/sbi/processor/time_zone.go @@ -0,0 +1,55 @@ +// internal/sbi/processor/time_zone.go +package processor + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +// HandleGetTimeZone 查詢時區 +func (p *Processor) HandleGetTimeZone(c *gin.Context, city string) { + tzData := p.Context().TimeZoneData + if tz, ok := tzData[city]; ok { + c.String(http.StatusOK, tz) + return + } + c.String(http.StatusNotFound, fmt.Sprintf("[%s] not found", city)) +} + +// TimeZoneRequest used for POST /city +type TimeZoneRequest struct { + City string `json:"City"` + TimeZone string `json:"TimeZone"` +} + +// HandleAddNewCityTimeZone 新增城市時區 +func (p *Processor) HandleAddNewCityTimeZone(c *gin.Context, req TimeZoneRequest) { + if _, ok := p.Context().TimeZoneData[req.City]; ok { + c.String(http.StatusConflict, fmt.Sprintf("City '%s' already exists", req.City)) + return + } + p.Context().TimeZoneData[req.City] = req.TimeZone + c.String(http.StatusOK, fmt.Sprintf("Time zone of %s is set to %s", req.City, req.TimeZone)) +} + +// HTTPResetCityTimeZone 重設時區 +func (p *Processor) HandleResetCityTimeZone(c *gin.Context, city string, newTZ string) { + if _, ok := p.Context().TimeZoneData[city]; !ok { + c.String(http.StatusNotFound, fmt.Sprintf("City '%s' not found", city)) + return + } + p.Context().TimeZoneData[city] = newTZ + c.String(http.StatusOK, fmt.Sprintf("Time zone of %s is reset to %s", city, newTZ)) +} + +// HTTPDeleteCityTimeZone 刪除城市時區 +func (p *Processor) HandleDeleteCityTimeZone(c *gin.Context, city string) { + if _, ok := p.Context().TimeZoneData[city]; !ok { + c.String(http.StatusNotFound, fmt.Sprintf("City '%s' not found", city)) + return + } + delete(p.Context().TimeZoneData, city) + c.String(http.StatusOK, fmt.Sprintf("City '%s' has been removed", city)) +} diff --git a/internal/sbi/processor/time_zone_test.go b/internal/sbi/processor/time_zone_test.go new file mode 100644 index 0000000..a77fd72 --- /dev/null +++ b/internal/sbi/processor/time_zone_test.go @@ -0,0 +1,270 @@ +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 setupTimeZoneProcessor(t *testing.T) (*processor.Processor, *processor.MockProcessorNf) { + gin.SetMode(gin.TestMode) + + mockCtrl := gomock.NewController(t) + processorNf := processor.NewMockProcessorNf(mockCtrl) + proc, err := processor.NewProcessor(processorNf) + if err != nil { + t.Fatalf("Failed to create processor: %s", err) + } + return proc, processorNf +} + +func Test_HandleGetTimeZone(t *testing.T) { + proc, processorNf := setupTimeZoneProcessor(t) + + t.Run("Get TimeZone for Existing City", func(t *testing.T) { + const INPUT_CITY = "Taipei" + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "UTC+8" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + }, + }) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleGetTimeZone(ginCtx, INPUT_CITY) + + 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("Get TimeZone for Non-Existing City", func(t *testing.T) { + const INPUT_CITY = "Unknown" + const EXPECTED_STATUS = 404 + const EXPECTED_BODY = "[Unknown] not found" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + }, + }) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleGetTimeZone(ginCtx, INPUT_CITY) + + 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_HandleAddNewCityTimeZone(t *testing.T) { + proc, processorNf := setupTimeZoneProcessor(t) + + t.Run("Add New City Successfully", func(t *testing.T) { + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "Time zone of Chicago is set to UTC-6" + + req := processor.TimeZoneRequest{ + City: "Chicago", + TimeZone: "UTC-6", + } + + timeZoneData := map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + } + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: timeZoneData, + }).Times(2) // Called twice: once to check if city exists, once to set timezone + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleAddNewCityTimeZone(ginCtx, req) + + 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()) + } + + // Verify city was added to the data + if timeZoneData["Chicago"] != "UTC-6" { + t.Errorf("Expected Chicago to be added with timezone UTC-6, but was not found") + } + }) + + t.Run("Add Existing City - Conflict", func(t *testing.T) { + const EXPECTED_STATUS = 409 + const EXPECTED_BODY = "City 'Taipei' already exists" + + req := processor.TimeZoneRequest{ + City: "Taipei", + TimeZone: "UTC+7", + } + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + }, + }).Times(1) // Called once to check if city exists (conflict case) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleAddNewCityTimeZone(ginCtx, req) + + 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_HandleResetCityTimeZone(t *testing.T) { + proc, processorNf := setupTimeZoneProcessor(t) + + t.Run("Reset Existing City TimeZone", func(t *testing.T) { + const INPUT_CITY = "Taipei" + const NEW_TIMEZONE = "UTC+7" + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "Time zone of Taipei is reset to UTC+7" + + timeZoneData := map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + } + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: timeZoneData, + }).Times(2) // Called twice: once to check if city exists, once to update timezone + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleResetCityTimeZone(ginCtx, INPUT_CITY, NEW_TIMEZONE) + + 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()) + } + + // Verify timezone was updated + if timeZoneData["Taipei"] != NEW_TIMEZONE { + t.Errorf("Expected Taipei timezone to be updated to %s, got %s", NEW_TIMEZONE, timeZoneData["Taipei"]) + } + }) + + t.Run("Reset Non-Existing City TimeZone", func(t *testing.T) { + const INPUT_CITY = "Unknown" + const NEW_TIMEZONE = "UTC+0" + const EXPECTED_STATUS = 404 + const EXPECTED_BODY = "City 'Unknown' not found" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + }, + }).Times(1) // Called once to check if city exists (not found case) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleResetCityTimeZone(ginCtx, INPUT_CITY, NEW_TIMEZONE) + + 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_HandleDeleteCityTimeZone(t *testing.T) { + proc, processorNf := setupTimeZoneProcessor(t) + + t.Run("Delete Existing City", func(t *testing.T) { + const INPUT_CITY = "Tokyo" + const EXPECTED_STATUS = 200 + const EXPECTED_BODY = "City 'Tokyo' has been removed" + + timeZoneData := map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + } + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: timeZoneData, + }).Times(2) // Called twice: once to check if city exists, once to delete from map + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleDeleteCityTimeZone(ginCtx, INPUT_CITY) + + 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()) + } + + // Verify city was deleted + if _, exists := timeZoneData["Tokyo"]; exists { + t.Errorf("Expected Tokyo to be deleted, but it still exists") + } + }) + + t.Run("Delete Non-Existing City", func(t *testing.T) { + const INPUT_CITY = "Unknown" + const EXPECTED_STATUS = 404 + const EXPECTED_BODY = "City 'Unknown' not found" + + processorNf.EXPECT().Context().Return(&nf_context.NFContext{ + TimeZoneData: map[string]string{ + "Taipei": "UTC+8", + "Tokyo": "UTC+9", + }, + }).Times(1) // Called once to check if city exists (not found case) + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + proc.HandleDeleteCityTimeZone(ginCtx, INPUT_CITY) + + 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 6631416..50f444f 100644 --- a/internal/sbi/router.go +++ b/internal/sbi/router.go @@ -61,6 +61,9 @@ func newRouter(s *Server) *gin.Engine { fortuneGroup := router.Group("/fortune") applyRoutes(fortuneGroup, s.getFortuneRoute()) + timeZoneGroup := router.Group("/timezone") + applyRoutes(timeZoneGroup, s.getTimeZoneRoute()) + return router }