Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type NFContext struct {

Fortunes []string
FortuneMutex sync.RWMutex

TimeZoneData map[string]string
}

type Message struct {
Expand Down Expand Up @@ -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 {
Expand Down
119 changes: 119 additions & 0 deletions internal/sbi/api_timezone.go
Original file line number Diff line number Diff line change
@@ -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)
}
243 changes: 243 additions & 0 deletions internal/sbi/api_timezone_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
Loading
Loading