diff --git a/.mockery.yaml b/.mockery.yaml index 4ef845a..f95b3b3 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -12,3 +12,4 @@ packages: RequestHandler: Connection: Request: + Channel: diff --git a/examples/http_backend/main.go b/examples/http_backend/main.go index 2f485a7..0dac4e8 100644 --- a/examples/http_backend/main.go +++ b/examples/http_backend/main.go @@ -12,6 +12,7 @@ import ( "github.com/ksysoev/wasabi/channel" "github.com/ksysoev/wasabi/dispatch" "github.com/ksysoev/wasabi/middleware/request" + "github.com/ksysoev/wasabi/server" ) const ( @@ -52,7 +53,7 @@ func main() { dispatcher.Use(ErrHandler) dispatcher.Use(request.NewTrottlerMiddleware(10)) - server := wasabi.NewServer(Port) + server := server.NewServer(Port) channel := channel.NewDefaultChannel("/", dispatcher, connRegistry) server.AddChannel(channel) diff --git a/interfaces.go b/interfaces.go index ba6a054..aa5bdee 100644 --- a/interfaces.go +++ b/interfaces.go @@ -2,6 +2,7 @@ package wasabi import ( "context" + "net/http" "golang.org/x/net/websocket" ) @@ -47,3 +48,10 @@ type Connection interface { type RequestHandler interface { Handle(conn Connection, req Request) error } + +// Channel is interface for channels +type Channel interface { + Path() string + SetContext(ctx context.Context) + Handler() http.Handler +} diff --git a/mocks/mock_Channel.go b/mocks/mock_Channel.go new file mode 100644 index 0000000..9c92f58 --- /dev/null +++ b/mocks/mock_Channel.go @@ -0,0 +1,164 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +//go:build !compile + +package mocks + +import ( + context "context" + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// MockChannel is an autogenerated mock type for the Channel type +type MockChannel struct { + mock.Mock +} + +type MockChannel_Expecter struct { + mock *mock.Mock +} + +func (_m *MockChannel) EXPECT() *MockChannel_Expecter { + return &MockChannel_Expecter{mock: &_m.Mock} +} + +// Handler provides a mock function with given fields: +func (_m *MockChannel) Handler() http.Handler { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Handler") + } + + var r0 http.Handler + if rf, ok := ret.Get(0).(func() http.Handler); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(http.Handler) + } + } + + return r0 +} + +// MockChannel_Handler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Handler' +type MockChannel_Handler_Call struct { + *mock.Call +} + +// Handler is a helper method to define mock.On call +func (_e *MockChannel_Expecter) Handler() *MockChannel_Handler_Call { + return &MockChannel_Handler_Call{Call: _e.mock.On("Handler")} +} + +func (_c *MockChannel_Handler_Call) Run(run func()) *MockChannel_Handler_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockChannel_Handler_Call) Return(_a0 http.Handler) *MockChannel_Handler_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChannel_Handler_Call) RunAndReturn(run func() http.Handler) *MockChannel_Handler_Call { + _c.Call.Return(run) + return _c +} + +// Path provides a mock function with given fields: +func (_m *MockChannel) Path() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Path") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockChannel_Path_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Path' +type MockChannel_Path_Call struct { + *mock.Call +} + +// Path is a helper method to define mock.On call +func (_e *MockChannel_Expecter) Path() *MockChannel_Path_Call { + return &MockChannel_Path_Call{Call: _e.mock.On("Path")} +} + +func (_c *MockChannel_Path_Call) Run(run func()) *MockChannel_Path_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockChannel_Path_Call) Return(_a0 string) *MockChannel_Path_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockChannel_Path_Call) RunAndReturn(run func() string) *MockChannel_Path_Call { + _c.Call.Return(run) + return _c +} + +// SetContext provides a mock function with given fields: ctx +func (_m *MockChannel) SetContext(ctx context.Context) { + _m.Called(ctx) +} + +// MockChannel_SetContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetContext' +type MockChannel_SetContext_Call struct { + *mock.Call +} + +// SetContext is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockChannel_Expecter) SetContext(ctx interface{}) *MockChannel_SetContext_Call { + return &MockChannel_SetContext_Call{Call: _e.mock.On("SetContext", ctx)} +} + +func (_c *MockChannel_SetContext_Call) Run(run func(ctx context.Context)) *MockChannel_SetContext_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockChannel_SetContext_Call) Return() *MockChannel_SetContext_Call { + _c.Call.Return() + return _c +} + +func (_c *MockChannel_SetContext_Call) RunAndReturn(run func(context.Context)) *MockChannel_SetContext_Call { + _c.Call.Return(run) + return _c +} + +// NewMockChannel creates a new instance of MockChannel. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockChannel(t interface { + mock.TestingT + Cleanup(func()) +}) *MockChannel { + mock := &MockChannel{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server.go b/server/server.go similarity index 61% rename from server.go rename to server/server.go index 02cf9b8..7a3ac5d 100644 --- a/server.go +++ b/server/server.go @@ -1,29 +1,27 @@ -package wasabi +package server import ( "context" + "fmt" "net/http" "strconv" + "sync" "time" + "github.com/ksysoev/wasabi" "golang.org/x/exp/slog" ) -// Channel is interface for channels -type Channel interface { - Path() string - SetContext(ctx context.Context) - Handler() http.Handler -} - const ( ReadHeaderTimeout = 3 * time.Second ReadTimeout = 30 * time.Second ) type Server struct { - channels []Channel - port uint16 + mutex *sync.Mutex + channels []wasabi.Channel + port uint16 + isRunning bool } // NewServer creates new instance of Wasabi server @@ -32,12 +30,13 @@ type Server struct { func NewServer(port uint16) *Server { return &Server{ port: port, - channels: make([]Channel, 0, 1), + channels: make([]wasabi.Channel, 0, 1), + mutex: &sync.Mutex{}, } } // AddChannel adds new channel to server -func (s *Server) AddChannel(channel Channel) { +func (s *Server) AddChannel(channel wasabi.Channel) { s.channels = append(s.channels, channel) } @@ -45,9 +44,17 @@ func (s *Server) AddChannel(channel Channel) { // ctx - context // returns error if any func (s *Server) Run(ctx context.Context) error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.isRunning { + return fmt.Errorf("server is already running") + } + listen := ":" + strconv.Itoa(int(s.port)) execCtx, cancel := context.WithCancel(ctx) + defer cancel() mux := http.NewServeMux() @@ -69,8 +76,28 @@ func (s *Server) Run(ctx context.Context) error { Handler: mux, } + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + <-execCtx.Done() + + slog.Info("Shutting down app server on " + listen) + + if err := server.Shutdown(context.Background()); err != nil { + slog.Error("Failed to shutdown app server", "error", err) + } + + wg.Done() + }() + err := server.ListenAndServe() - if err != nil { + + cancel() + + wg.Wait() + + if err != http.ErrServerClosed { return err } diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..2f9abeb --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,71 @@ +package server + +import ( + "context" + "testing" + "time" + + "github.com/ksysoev/wasabi/mocks" +) + +func TestServer_Shutdown_with_context(t *testing.T) { + // Create a new Server instance + server := NewServer(0) + + // Create a new context + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + doneChan := make(chan struct{}) + // Start the server in a separate goroutine + go func() { + _ = server.Run(ctx) + + close(doneChan) + }() + + cancel() + + select { + case <-doneChan: + case <-time.After(1 * time.Second): + t.Error("Server did not stop") + } +} + +func TestNewServer(t *testing.T) { + port := uint16(8080) + server := NewServer(port) + + if server.port != port { + t.Errorf("Expected port %d, but got %d", port, server.port) + } + + if len(server.channels) != 0 { + t.Errorf("Expected empty channels slice, but got %d channels", len(server.channels)) + } + + if server.mutex == nil { + t.Error("Expected non-nil mutex") + } +} +func TestServer_AddChannel(t *testing.T) { + // Create a new Server instance + server := NewServer(0) + + // Create a new channel + channel := mocks.NewMockChannel(t) + channel.EXPECT().Path().Return("test") + + // Add the channel to the server + server.AddChannel(channel) + + // Check if the channel was added correctly + if len(server.channels) != 1 { + t.Errorf("Expected 1 channel, but got %d channels", len(server.channels)) + } + + if server.channels[0].Path() != "test" { + t.Errorf("Expected channel name 'test', but got '%s'", server.channels[0].Path()) + } +}