From 6aa0680b89961510ce86e3b9f92c9b4823112515 Mon Sep 17 00:00:00 2001 From: carlhus Date: Wed, 8 Oct 2025 06:21:09 +0000 Subject: [PATCH 1/2] feat: add time zone query handling --- internal/context/context.go | 15 ++ internal/sbi/api_timezone.go | 116 ++++++++++ internal/sbi/api_timezone_test.go | 231 +++++++++++++++++++ internal/sbi/processor/time_zone.go | 55 +++++ internal/sbi/processor/time_zone_test.go | 270 +++++++++++++++++++++++ internal/sbi/router.go | 3 + 6 files changed, 690 insertions(+) create mode 100644 internal/sbi/api_timezone.go create mode 100644 internal/sbi/api_timezone_test.go create mode 100644 internal/sbi/processor/time_zone.go create mode 100644 internal/sbi/processor/time_zone_test.go diff --git a/internal/context/context.go b/internal/context/context.go index 3d3b928..e7ad293 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..d81f66a --- /dev/null +++ b/internal/sbi/api_timezone.go @@ -0,0 +1,116 @@ +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) +} \ No newline at end of file diff --git a/internal/sbi/api_timezone_test.go b/internal/sbi/api_timezone_test.go new file mode 100644 index 0000000..fa004b2 --- /dev/null +++ b/internal/sbi/api_timezone_test.go @@ -0,0 +1,231 @@ +package sbi_test + +import ( + "bytes" + "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 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 := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + + var err error + ginCtx.Request, err = http.NewRequest("GET", "/timezone/city/", nil) + if err != nil { + t.Errorf("Failed to create request: %s", err) + 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 := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + ginCtx.Params = gin.Params{gin.Param{Key: "City", Value: "Taipei"}} + + invalidJSON := bytes.NewBufferString("{invalid json}") + var err error + ginCtx.Request, err = http.NewRequest("POST", "/timezone/city/Taipei", invalidJSON) + 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("Missing TimeZone field", func(t *testing.T) { + const EXPECTED_STATUS = http.StatusBadRequest + const EXPECTED_BODY = "TimeZone field is required" + + httpRecorder := httptest.NewRecorder() + ginCtx, _ := gin.CreateTestContext(httpRecorder) + ginCtx.Params = gin.Params{gin.Param{Key: "City", Value: "Taipei"}} + + emptyJSON := bytes.NewBufferString(`{"TimeZone": ""}`) + var err error + ginCtx.Request, err = http.NewRequest("POST", "/timezone/city/Taipei", emptyJSON) + 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()) + } + }) +} + +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()) + } + }) +} \ No newline at end of file diff --git a/internal/sbi/processor/time_zone.go b/internal/sbi/processor/time_zone.go new file mode 100644 index 0000000..065b9a7 --- /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)) +} \ No newline at end of file diff --git a/internal/sbi/processor/time_zone_test.go b/internal/sbi/processor/time_zone_test.go new file mode 100644 index 0000000..514e141 --- /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()) + } + }) +} \ No newline at end of file diff --git a/internal/sbi/router.go b/internal/sbi/router.go index 6631416..eaee7e3 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 } From 57e5cc5940696a3a3d11a983ee780c4e524f166b Mon Sep 17 00:00:00 2001 From: carlhus Date: Wed, 8 Oct 2025 10:37:49 +0000 Subject: [PATCH 2/2] fix: update lint in time_zone api --- internal/context/context.go | 10 +-- internal/sbi/api_timezone.go | 99 ++++++++++++------------ internal/sbi/api_timezone_test.go | 68 +++++++++------- internal/sbi/processor/time_zone.go | 50 ++++++------ internal/sbi/processor/time_zone_test.go | 2 +- internal/sbi/router.go | 2 +- 6 files changed, 123 insertions(+), 108 deletions(-) diff --git a/internal/context/context.go b/internal/context/context.go index e7ad293..0d1e5cd 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -38,7 +38,7 @@ type NFContext struct { Fortunes []string FortuneMutex sync.RWMutex - TimeZoneData map[string]string + TimeZoneData map[string]string } type Message struct { @@ -114,17 +114,17 @@ func InitNfContext() { } nfContext.TimeZoneData = map[string]string{ - "Taipei": "UTC+8", - "Tokyo": "UTC+9", + "Taipei": "UTC+8", + "Tokyo": "UTC+9", "Seoul": "UTC+9", - "NewYork": "UTC-5", + "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 index d81f66a..a19d600 100644 --- a/internal/sbi/api_timezone.go +++ b/internal/sbi/api_timezone.go @@ -1,4 +1,5 @@ package sbi + import ( "net/http" @@ -10,39 +11,39 @@ import ( 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") - }, + 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, @@ -57,12 +58,12 @@ func (s *Server) getTimeZoneRoute() []Route { func (s *Server) HTTPGetTimeZoneByCity(c *gin.Context) { logger.SBILog.Infof("In HTTPGetTimeZoneByCity") - city := c.Param("City") + city := c.Param("City") if city == "" { c.String(http.StatusBadRequest, "No city provided") return } - s.Processor().HandleGetTimeZone(c, city) + s.Processor().HandleGetTimeZone(c, city) } func (s *Server) HTTPAddNewCityTimeZone(c *gin.Context) { @@ -71,46 +72,48 @@ func (s *Server) HTTPAddNewCityTimeZone(c *gin.Context) { var req processor.TimeZoneRequest if err := c.ShouldBindJSON(&req); err != nil { c.String(http.StatusBadRequest, "Invalid JSON") - return + return } if req.City == "" || req.TimeZone == "" { c.String(http.StatusBadRequest, "City and TimeZone fields are required") return } - s.Processor().HandleAddNewCityTimeZone(c, req) + s.Processor().HandleAddNewCityTimeZone(c, req) } func (s *Server) HTTPResetCityTimeZone(c *gin.Context) { logger.SBILog.Infof("In HTTPResetCityTimeZone") - city := c.Param("City") + 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 - } + 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 - } + if req.TZ == "" { + c.String(http.StatusBadRequest, "TimeZone field is required") + return + } - s.Processor().HandleResetCityTimeZone(c, city, req.TZ) + 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) -} \ No newline at end of file + + 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 index fa004b2..fa52db9 100644 --- a/internal/sbi/api_timezone_test.go +++ b/internal/sbi/api_timezone_test.go @@ -2,6 +2,7 @@ package sbi_test import ( "bytes" + "io" "net/http" "net/http/httptest" "testing" @@ -27,6 +28,36 @@ func setupTimeZoneTestServer(t *testing.T) *sbi.Server { 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) @@ -34,13 +65,8 @@ func Test_HTTPGetTimeZoneByCity(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("GET", "/timezone/city/", nil) - if err != nil { - t.Errorf("Failed to create request: %s", err) + httpRecorder, ginCtx := createJSONRequest(t, "GET", "/timezone/city/", "", nil) + if ginCtx == nil { return } @@ -148,18 +174,11 @@ func Test_HTTPResetCityTimeZone(t *testing.T) { const EXPECTED_STATUS = http.StatusBadRequest const EXPECTED_BODY = "Invalid JSON format, expected object with TimeZone field" - httpRecorder := httptest.NewRecorder() - ginCtx, _ := gin.CreateTestContext(httpRecorder) - ginCtx.Params = gin.Params{gin.Param{Key: "City", Value: "Taipei"}} - - invalidJSON := bytes.NewBufferString("{invalid json}") - var err error - ginCtx.Request, err = http.NewRequest("POST", "/timezone/city/Taipei", invalidJSON) - if err != nil { - t.Errorf("Failed to create request: %s", err) + httpRecorder, ginCtx := createJSONRequest(t, "POST", "/timezone/city/Taipei", + "{invalid json}", gin.Params{gin.Param{Key: "City", Value: "Taipei"}}) + if ginCtx == nil { return } - ginCtx.Request.Header.Set("Content-Type", "application/json") server.HTTPResetCityTimeZone(ginCtx) @@ -176,18 +195,11 @@ func Test_HTTPResetCityTimeZone(t *testing.T) { const EXPECTED_STATUS = http.StatusBadRequest const EXPECTED_BODY = "TimeZone field is required" - httpRecorder := httptest.NewRecorder() - ginCtx, _ := gin.CreateTestContext(httpRecorder) - ginCtx.Params = gin.Params{gin.Param{Key: "City", Value: "Taipei"}} - - emptyJSON := bytes.NewBufferString(`{"TimeZone": ""}`) - var err error - ginCtx.Request, err = http.NewRequest("POST", "/timezone/city/Taipei", emptyJSON) - if err != nil { - t.Errorf("Failed to create request: %s", err) + httpRecorder, ginCtx := createJSONRequest(t, "POST", "/timezone/city/Taipei", + `{"TimeZone": ""}`, gin.Params{gin.Param{Key: "City", Value: "Taipei"}}) + if ginCtx == nil { return } - ginCtx.Request.Header.Set("Content-Type", "application/json") server.HTTPResetCityTimeZone(ginCtx) @@ -228,4 +240,4 @@ func Test_HTTPDeleteCityTimeZone(t *testing.T) { t.Errorf("Expected body %s, got %s", EXPECTED_BODY, httpRecorder.Body.String()) } }) -} \ No newline at end of file +} diff --git a/internal/sbi/processor/time_zone.go b/internal/sbi/processor/time_zone.go index 065b9a7..d3ca302 100644 --- a/internal/sbi/processor/time_zone.go +++ b/internal/sbi/processor/time_zone.go @@ -2,26 +2,26 @@ package processor import ( - "fmt" - "net/http" + "fmt" + "net/http" - "github.com/gin-gonic/gin" + "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)) + 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"` + City string `json:"City"` + TimeZone string `json:"TimeZone"` } // HandleAddNewCityTimeZone 新增城市時區 @@ -31,25 +31,25 @@ func (p *Processor) HandleAddNewCityTimeZone(c *gin.Context, req TimeZoneRequest 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)) + 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)) + 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)) -} \ No newline at end of file + 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 index 514e141..a77fd72 100644 --- a/internal/sbi/processor/time_zone_test.go +++ b/internal/sbi/processor/time_zone_test.go @@ -267,4 +267,4 @@ func Test_HandleDeleteCityTimeZone(t *testing.T) { t.Errorf("Expected body %s, got %s", EXPECTED_BODY, httpRecorder.Body.String()) } }) -} \ No newline at end of file +} diff --git a/internal/sbi/router.go b/internal/sbi/router.go index eaee7e3..50f444f 100644 --- a/internal/sbi/router.go +++ b/internal/sbi/router.go @@ -62,7 +62,7 @@ func newRouter(s *Server) *gin.Engine { applyRoutes(fortuneGroup, s.getFortuneRoute()) timeZoneGroup := router.Group("/timezone") - applyRoutes(timeZoneGroup, s.getTimeZoneRoute()) + applyRoutes(timeZoneGroup, s.getTimeZoneRoute()) return router }