mirror of
https://github.com/hay-kot/homebox.git
synced 2025-07-29 22:00:27 +00:00
Initial commit
This commit is contained in:
commit
29f583e936
135 changed files with 18463 additions and 0 deletions
7
backend/pkgs/server/constants.go
Normal file
7
backend/pkgs/server/constants.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package server
|
||||
|
||||
const (
|
||||
ContentType = "Content-Type"
|
||||
ContentJSON = "application/json"
|
||||
ContentXML = "application/xml"
|
||||
)
|
48
backend/pkgs/server/request.go
Normal file
48
backend/pkgs/server/request.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Decode reads the body of an HTTP request looking for a JSON document. The
|
||||
// body is decoded into the provided value.
|
||||
func Decode(r *http.Request, val interface{}) error {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(val); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetId is a shotcut to get the id from the request URL or return a default value
|
||||
func GetParam(r *http.Request, key, d string) string {
|
||||
val := r.URL.Query().Get(key)
|
||||
|
||||
if val == "" {
|
||||
return d
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// GetSkip is a shotcut to get the skip from the request URL parameters
|
||||
func GetSkip(r *http.Request, d string) string {
|
||||
return GetParam(r, "skip", d)
|
||||
}
|
||||
|
||||
// GetSkip is a shotcut to get the skip from the request URL parameters
|
||||
func GetId(r *http.Request, d string) string {
|
||||
return GetParam(r, "id", d)
|
||||
}
|
||||
|
||||
// GetLimit is a shotcut to get the limit from the request URL parameters
|
||||
func GetLimit(r *http.Request, d string) string {
|
||||
return GetParam(r, "limit", d)
|
||||
}
|
||||
|
||||
// GetQuery is a shotcut to get the sort from the request URL parameters
|
||||
func GetQuery(r *http.Request, d string) string {
|
||||
return GetParam(r, "query", d)
|
||||
}
|
210
backend/pkgs/server/request_test.go
Normal file
210
backend/pkgs/server/request_test.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestStruct struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
type args struct {
|
||||
r *http.Request
|
||||
val interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "check_error",
|
||||
args: args{
|
||||
r: &http.Request{
|
||||
Body: http.NoBody,
|
||||
},
|
||||
val: make(map[string]interface{}),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "check_success",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
val: TestStruct{
|
||||
Name: "test",
|
||||
Data: "test",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Decode(tt.args.r, &tt.args.val); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetParam(t *testing.T) {
|
||||
type args struct {
|
||||
r *http.Request
|
||||
key string
|
||||
d string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "check_default",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
key: "id",
|
||||
d: "default",
|
||||
},
|
||||
want: "default",
|
||||
},
|
||||
{
|
||||
name: "check_id",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/item?id=123", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
key: "id",
|
||||
d: "",
|
||||
},
|
||||
want: "123",
|
||||
},
|
||||
{
|
||||
name: "check_query",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/item?query=hello-world", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
key: "query",
|
||||
d: "",
|
||||
},
|
||||
want: "hello-world",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetParam(tt.args.r, tt.args.key, tt.args.d); got != tt.want {
|
||||
t.Errorf("GetParam() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSkip(t *testing.T) {
|
||||
type args struct {
|
||||
r *http.Request
|
||||
d string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "check_default",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
d: "0",
|
||||
},
|
||||
want: "0",
|
||||
},
|
||||
{
|
||||
name: "check_skip",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/item?skip=107", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
d: "0",
|
||||
},
|
||||
want: "107",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetSkip(tt.args.r, tt.args.d); got != tt.want {
|
||||
t.Errorf("GetSkip() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLimit(t *testing.T) {
|
||||
type args struct {
|
||||
r *http.Request
|
||||
d string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "check_default",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
d: "0",
|
||||
},
|
||||
want: "0",
|
||||
},
|
||||
{
|
||||
name: "check_limit",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/item?limit=107", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
d: "0",
|
||||
},
|
||||
want: "107",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetLimit(tt.args.r, tt.args.d); got != tt.want {
|
||||
t.Errorf("GetLimit() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetQuery(t *testing.T) {
|
||||
type args struct {
|
||||
r *http.Request
|
||||
d string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "check_default",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
d: "0",
|
||||
},
|
||||
want: "0",
|
||||
},
|
||||
{
|
||||
name: "check_query",
|
||||
args: args{
|
||||
r: httptest.NewRequest("POST", "/item?query=hello-query", strings.NewReader(`{"name":"test","data":"test"}`)),
|
||||
d: "0",
|
||||
},
|
||||
want: "hello-query",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := GetQuery(tt.args.r, tt.args.d); got != tt.want {
|
||||
t.Errorf("GetQuery() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
61
backend/pkgs/server/response.go
Normal file
61
backend/pkgs/server/response.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Respond converts a Go value to JSON and sends it to the client.
|
||||
// Adapted from https://github.com/ardanlabs/service/tree/master/foundation/web
|
||||
func Respond(w http.ResponseWriter, statusCode int, data interface{}) error {
|
||||
// If there is nothing to marshal then set status code and return.
|
||||
if statusCode == http.StatusNoContent {
|
||||
w.WriteHeader(statusCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert the response value to JSON.
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the content type and headers once we know marshaling has succeeded.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Write the status code to the response.
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
// Send the result back to the client.
|
||||
if _, err := w.Write(jsonData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResponseError is a helper function that sends a JSON response of an error message
|
||||
func RespondError(w http.ResponseWriter, statusCode int, err error) {
|
||||
eb := ErrorBuilder{}
|
||||
eb.AddError(err)
|
||||
eb.Respond(w, statusCode)
|
||||
}
|
||||
|
||||
// RespondInternalServerError is a wrapper around RespondError that sends a 500 internal server error. Useful for
|
||||
// Sending generic errors when everything went wrong.
|
||||
func RespondInternalServerError(w http.ResponseWriter) {
|
||||
RespondError(w, http.StatusInternalServerError, errors.New("internal server error"))
|
||||
}
|
||||
|
||||
// RespondNotFound is a helper utility for responding with a generic
|
||||
// "unauthorized" error.
|
||||
func RespondUnauthorized(w http.ResponseWriter) {
|
||||
RespondError(w, http.StatusUnauthorized, errors.New("unauthorized"))
|
||||
}
|
||||
|
||||
// RespondForbidden is a helper utility for responding with a generic
|
||||
// "forbidden" error.
|
||||
func RespondForbidden(w http.ResponseWriter) {
|
||||
RespondError(w, http.StatusForbidden, errors.New("forbidden"))
|
||||
}
|
51
backend/pkgs/server/response_error_builder.go
Normal file
51
backend/pkgs/server/response_error_builder.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ErrorBuilder is a helper type to build a response that contains an array of errors.
|
||||
// Typical use cases are for returning an array of validation errors back to the user.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
//
|
||||
// {
|
||||
// "errors": [
|
||||
// "invalid id",
|
||||
// "invalid name",
|
||||
// "invalid description"
|
||||
// ],
|
||||
// "message": "Unprocessable Entity",
|
||||
// "status": 422
|
||||
// }
|
||||
//
|
||||
type ErrorBuilder struct {
|
||||
errs []string
|
||||
}
|
||||
|
||||
// HasErrors returns true if the ErrorBuilder has any errors.
|
||||
func (eb *ErrorBuilder) HasErrors() bool {
|
||||
if (eb.errs == nil) || (len(eb.errs) == 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AddError adds an error to the ErrorBuilder if an error is not nil. If the
|
||||
// Error is nil, then nothing is added.
|
||||
func (eb *ErrorBuilder) AddError(err error) {
|
||||
if err != nil {
|
||||
if eb.errs == nil {
|
||||
eb.errs = make([]string, 0)
|
||||
}
|
||||
|
||||
eb.errs = append(eb.errs, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Respond sends a JSON response with the ErrorBuilder's errors. If there are no errors, then
|
||||
// the errors field will be an empty array.
|
||||
func (eb *ErrorBuilder) Respond(w http.ResponseWriter, statusCode int) {
|
||||
Respond(w, statusCode, Wrap(nil).AddError(http.StatusText(statusCode), eb.errs))
|
||||
}
|
107
backend/pkgs/server/response_error_builder_test.go
Normal file
107
backend/pkgs/server/response_error_builder_test.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hay-kot/git-web-template/backend/pkgs/faker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ErrorBuilder_HasErrors_NilList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var ebNilList = ErrorBuilder{}
|
||||
assert.False(t, ebNilList.HasErrors(), "ErrorBuilder.HasErrors() should return false when list is nil")
|
||||
|
||||
}
|
||||
|
||||
func Test_ErrorBuilder_HasErrors_EmptyList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var ebEmptyList = ErrorBuilder{
|
||||
errs: []string{},
|
||||
}
|
||||
assert.False(t, ebEmptyList.HasErrors(), "ErrorBuilder.HasErrors() should return false when list is empty")
|
||||
|
||||
}
|
||||
|
||||
func Test_ErrorBuilder_HasErrors_WithError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var ebList = ErrorBuilder{}
|
||||
ebList.AddError(errors.New("test error"))
|
||||
|
||||
assert.True(t, ebList.HasErrors(), "ErrorBuilder.HasErrors() should return true when list is not empty")
|
||||
|
||||
}
|
||||
|
||||
func Test_ErrorBuilder_AddError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
randomError := make([]error, 10)
|
||||
|
||||
f := faker.NewFaker()
|
||||
|
||||
errorStrings := make([]string, 10)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
err := errors.New(f.RandomString(10))
|
||||
randomError[i] = err
|
||||
errorStrings[i] = err.Error()
|
||||
}
|
||||
|
||||
// Check Results
|
||||
var ebList = ErrorBuilder{}
|
||||
|
||||
for _, err := range randomError {
|
||||
ebList.AddError(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, errorStrings, ebList.errs, "ErrorBuilder.AddError() should add an error to the list")
|
||||
}
|
||||
|
||||
func Test_ErrorBuilder_Respond(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := faker.NewFaker()
|
||||
|
||||
randomError := make([]error, 5)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
err := errors.New(f.RandomString(5))
|
||||
randomError[i] = err
|
||||
}
|
||||
|
||||
// Check Results
|
||||
var ebList = ErrorBuilder{}
|
||||
|
||||
for _, err := range randomError {
|
||||
ebList.AddError(err)
|
||||
}
|
||||
|
||||
fakeWriter := httptest.NewRecorder()
|
||||
|
||||
ebList.Respond(fakeWriter, 422)
|
||||
|
||||
assert.Equal(t, 422, fakeWriter.Code, "ErrorBuilder.Respond() should return a status code of 422")
|
||||
|
||||
// Check errors payload is correct
|
||||
|
||||
errorsStruct := struct {
|
||||
Errors []string `json:"details"`
|
||||
Message string `json:"message"`
|
||||
Error bool `json:"error"`
|
||||
}{
|
||||
Errors: ebList.errs,
|
||||
Message: http.StatusText(http.StatusUnprocessableEntity),
|
||||
Error: true,
|
||||
}
|
||||
|
||||
asJson, _ := json.Marshal(errorsStruct)
|
||||
assert.JSONEq(t, string(asJson), fakeWriter.Body.String(), "ErrorBuilder.Respond() should return a JSON response with the errors")
|
||||
|
||||
}
|
78
backend/pkgs/server/response_test.go
Normal file
78
backend/pkgs/server/response_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Respond_NoContent(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
dummystruct := struct {
|
||||
Name string
|
||||
}{
|
||||
Name: "dummy",
|
||||
}
|
||||
|
||||
Respond(recorder, http.StatusNoContent, dummystruct)
|
||||
|
||||
assert.Equal(t, http.StatusNoContent, recorder.Code)
|
||||
assert.Empty(t, recorder.Body.String())
|
||||
}
|
||||
|
||||
func Test_Respond_JSON(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
dummystruct := struct {
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
Name: "dummy",
|
||||
}
|
||||
|
||||
Respond(recorder, http.StatusCreated, dummystruct)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, recorder.Code)
|
||||
assert.JSONEq(t, recorder.Body.String(), `{"name":"dummy"}`)
|
||||
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
|
||||
|
||||
}
|
||||
|
||||
func Test_RespondError(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
var customError = errors.New("custom error")
|
||||
|
||||
RespondError(recorder, http.StatusBadRequest, customError)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["custom error"], "message":"Bad Request", "error":true}`)
|
||||
|
||||
}
|
||||
func Test_RespondInternalServerError(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
RespondInternalServerError(recorder)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, recorder.Code)
|
||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["internal server error"], "message":"Internal Server Error", "error":true}`)
|
||||
|
||||
}
|
||||
func Test_RespondUnauthorized(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
RespondUnauthorized(recorder)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["unauthorized"], "message":"Unauthorized", "error":true}`)
|
||||
|
||||
}
|
||||
func Test_RespondForbidden(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
RespondForbidden(recorder)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, recorder.Code)
|
||||
assert.JSONEq(t, recorder.Body.String(), `{"details":["forbidden"], "message":"Forbidden", "error":true}`)
|
||||
|
||||
}
|
27
backend/pkgs/server/result.go
Normal file
27
backend/pkgs/server/result.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package server
|
||||
|
||||
type Result struct {
|
||||
Error bool `json:"error,omitempty"`
|
||||
Details interface{} `json:"details,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Item interface{} `json:"item,omitempty"`
|
||||
}
|
||||
|
||||
// Wrap creates a Wrapper instance and adds the initial namespace and data to be returned.
|
||||
func Wrap(data interface{}) Result {
|
||||
return Result{
|
||||
Item: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Result) AddMessage(message string) Result {
|
||||
r.Message = message
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Result) AddError(err string, details interface{}) Result {
|
||||
r.Message = err
|
||||
r.Details = details
|
||||
r.Error = true
|
||||
return r
|
||||
}
|
123
backend/pkgs/server/server.go
Normal file
123
backend/pkgs/server/server.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: #2 Implement Go routine pool/job queue
|
||||
|
||||
var ErrServerNotStarted = errors.New("server not started")
|
||||
var ErrServerAlreadyStarted = errors.New("server already started")
|
||||
|
||||
type Server struct {
|
||||
Host string
|
||||
Port string
|
||||
|
||||
Worker Worker
|
||||
wg sync.WaitGroup
|
||||
|
||||
started bool
|
||||
activeServer *http.Server
|
||||
}
|
||||
|
||||
func NewServer(host, port string) *Server {
|
||||
return &Server{
|
||||
Host: host,
|
||||
Port: port,
|
||||
wg: sync.WaitGroup{},
|
||||
Worker: NewSimpleWorker(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(sig string) error {
|
||||
if !s.started {
|
||||
return ErrServerNotStarted
|
||||
}
|
||||
fmt.Printf("Received %s signal, shutting down\n", sig)
|
||||
|
||||
// Create a context with a 5-second timeout.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := s.activeServer.Shutdown(ctx)
|
||||
s.started = false
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Http server shutdown, waiting for all tasks to finish")
|
||||
s.wg.Wait()
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) Start(router http.Handler) error {
|
||||
if s.started {
|
||||
return ErrServerAlreadyStarted
|
||||
}
|
||||
|
||||
s.activeServer = &http.Server{
|
||||
Addr: s.Host + ":" + s.Port,
|
||||
Handler: router,
|
||||
IdleTimeout: time.Minute,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
shutdownError := make(chan error)
|
||||
|
||||
go func() {
|
||||
// Create a quit channel which carries os.Signal values.
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
||||
// Use signal.Notify() to listen for incoming SIGINT and SIGTERM signals and
|
||||
// relay them to the quit channel.
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Read the signal from the quit channel. block until received
|
||||
sig := <-quit
|
||||
|
||||
err := s.Shutdown(sig.String())
|
||||
if err != nil {
|
||||
shutdownError <- err
|
||||
}
|
||||
|
||||
// Exit the application with a 0 (success) status code.
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
s.started = true
|
||||
err := s.activeServer.ListenAndServe()
|
||||
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = <-shutdownError
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("Server shutdown successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Background starts a go routine that runs on the servers pool. In the event of a shutdown
|
||||
// request, the server will wait until all open goroutines have finished before shutting down.
|
||||
func (svr *Server) Background(task func()) {
|
||||
svr.wg.Add(1)
|
||||
svr.Worker.Add(func() {
|
||||
defer svr.wg.Done()
|
||||
task()
|
||||
})
|
||||
}
|
97
backend/pkgs/server/server_test.go
Normal file
97
backend/pkgs/server/server_test.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testServer(t *testing.T, r http.Handler) *Server {
|
||||
svr := NewServer("127.0.0.1", "19245")
|
||||
|
||||
go func() {
|
||||
svr.Start(r)
|
||||
}()
|
||||
|
||||
ping := func() error {
|
||||
_, err := http.Get("http://127.0.0.1:19245")
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
if err := ping(); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
|
||||
return svr
|
||||
}
|
||||
|
||||
func Test_ServerShutdown_Error(t *testing.T) {
|
||||
svr := NewServer("127.0.0.1", "19245")
|
||||
|
||||
err := svr.Shutdown("test")
|
||||
assert.ErrorIs(t, err, ErrServerNotStarted)
|
||||
}
|
||||
|
||||
func Test_ServerStarts_Error(t *testing.T) {
|
||||
svr := testServer(t, nil)
|
||||
|
||||
err := svr.Start(nil)
|
||||
assert.ErrorIs(t, err, ErrServerAlreadyStarted)
|
||||
|
||||
err = svr.Shutdown("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_ServerStarts(t *testing.T) {
|
||||
svr := testServer(t, nil)
|
||||
err := svr.Shutdown("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_GracefulServerShutdownWithWorkers(t *testing.T) {
|
||||
isFinished := false
|
||||
|
||||
svr := testServer(t, nil)
|
||||
|
||||
svr.Background(func() {
|
||||
time.Sleep(time.Second * 4)
|
||||
isFinished = true
|
||||
})
|
||||
|
||||
err := svr.Shutdown("test")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isFinished)
|
||||
|
||||
}
|
||||
|
||||
func Test_GracefulServerShutdownWithRequests(t *testing.T) {
|
||||
isFinished := false
|
||||
|
||||
router := http.NewServeMux()
|
||||
|
||||
// add long running handler func
|
||||
router.HandleFunc("/test", func(rw http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(time.Second * 3)
|
||||
isFinished = true
|
||||
})
|
||||
|
||||
svr := testServer(t, router)
|
||||
|
||||
// Make request to "/test"
|
||||
go func() {
|
||||
http.Get("http://127.0.0.1:19245/test") // This is probably bad?
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second) // Hack to wait for the request to be made
|
||||
|
||||
err := svr.Shutdown("test")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, isFinished)
|
||||
}
|
20
backend/pkgs/server/worker.go
Normal file
20
backend/pkgs/server/worker.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package server
|
||||
|
||||
type Worker interface {
|
||||
Add(func())
|
||||
}
|
||||
|
||||
// SimpleWorker is a simple background worker that implements
|
||||
// the Worker interface and runs all tasks in a go routine without
|
||||
// a pool or que or limits. It's useful for simple or small applications
|
||||
// with minimal/short background tasks
|
||||
type SimpleWorker struct {
|
||||
}
|
||||
|
||||
func NewSimpleWorker() *SimpleWorker {
|
||||
return &SimpleWorker{}
|
||||
}
|
||||
|
||||
func (sw *SimpleWorker) Add(task func()) {
|
||||
go task()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue