diff --git a/server/api/handler/common.go b/server/api/handler/common.go index 521d4f1..d4ac9a3 100644 --- a/server/api/handler/common.go +++ b/server/api/handler/common.go @@ -1,13 +1,16 @@ package handler import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/logic" "bytes" "errors" "github.com/gin-gonic/gin" sqlite3 "github.com/mattn/go-sqlite3" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" "net/http" + "time" ) type CommonHandler struct { @@ -106,7 +109,7 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse { // @Failure 500 {object} ginresp.apiError // // @Router /api/health [get] -func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse { +func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse { type response struct { Status string `json:"status"` } @@ -125,6 +128,45 @@ func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse { return ginresp.JSON(http.StatusOK, response{Status: "ok"}) } +// Sleep swaggerdoc +// +// @Summary Return 200 after x seconds +// @ID api-common-sleep +// @Tags Common +// +// @Success 200 {object} handler.Sleep.response +// @Failure 400 {object} ginresp.apiError +// @Failure 500 {object} ginresp.apiError +// +// @Router /api/sleep/{secs} [post] +func (h CommonHandler) Sleep(g *gin.Context) ginresp.HTTPResponse { + type uri struct { + Seconds float64 `uri:"secs"` + } + type response struct { + Start string `json:"start"` + End string `json:"end"` + Duration float64 `json:"duration"` + } + + t0 := time.Now().Format(time.RFC3339Nano) + + var u uri + if err := g.ShouldBindUri(&u); err != nil { + return ginresp.APIError(g, 400, apierr.BINDFAIL_URI_PARAM, "Failed to read uri", err) + } + + time.Sleep(timeext.FromSecondsFloat64(u.Seconds)) + + t1 := time.Now().Format(time.RFC3339Nano) + + return ginresp.JSON(http.StatusOK, response{ + Start: t0, + End: t1, + Duration: u.Seconds, + }) +} + func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse { return ginresp.JSON(http.StatusNotFound, gin.H{ "": "================ ROUTE NOT FOUND ================", diff --git a/server/api/router.go b/server/api/router.go index a50d7c5..3fb3fbe 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -53,6 +53,7 @@ func (r *Router) Init(e *gin.Engine) { commonAPI.Any("/ping", ginresp.Wrap(r.commonHandler.Ping)) commonAPI.POST("/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest)) commonAPI.GET("/health", ginresp.Wrap(r.commonHandler.Health)) + commonAPI.POST("/sleep/:secs", ginresp.Wrap(r.commonHandler.Sleep)) } // ================ Swagger ================ diff --git a/server/cmd/scnserver/main.go b/server/cmd/scnserver/main.go index 732e6bd..4dbc5f7 100644 --- a/server/cmd/scnserver/main.go +++ b/server/cmd/scnserver/main.go @@ -20,7 +20,7 @@ func main() { log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace)) - sqlite, err := db.NewDatabase(conf) + sqlite, err := db.NewDatabase(conf.DBFile) if err != nil { panic(err) } diff --git a/server/db/database.go b/server/db/database.go index 8ee359f..3e5b5bb 100644 --- a/server/db/database.go +++ b/server/db/database.go @@ -1,7 +1,6 @@ package db import ( - scn "blackforestbytes.com/simplecloudnotifier" "blackforestbytes.com/simplecloudnotifier/db/schema" "context" "database/sql" @@ -15,8 +14,8 @@ type Database struct { db *sql.DB } -func NewDatabase(conf scn.Config) (*Database, error) { - db, err := sql.Open("sqlite3", conf.DBFile) +func NewDatabase(filename string) (*Database, error) { + db, err := sql.Open("sqlite3", filename) if err != nil { return nil, err } diff --git a/server/logic/application.go b/server/logic/application.go index 0b10886..1e31c5c 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -29,11 +29,14 @@ type Application struct { Database *db.Database Firebase push.NotificationClient Jobs []Job + stopChan chan bool + Port string } func NewApp(db *db.Database) *Application { return &Application{ Database: db, + stopChan: make(chan bool, 8), } } @@ -44,6 +47,10 @@ func (app *Application) Init(cfg scn.Config, g *gin.Engine, fb push.Notification app.Jobs = jobs } +func (app *Application) Stop() { + app.stopChan <- true +} + func (app *Application) Run() { httpserver := &http.Server{ Addr: net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort), @@ -53,8 +60,24 @@ func (app *Application) Run() { errChan := make(chan error) go func() { - log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + app.Config.ServerPort) - errChan <- httpserver.ListenAndServe() + + ln, err := net.Listen("tcp", httpserver.Addr) + if err != nil { + errChan <- err + return + } + + _, port, err := net.SplitHostPort(ln.Addr().String()) + if err != nil { + errChan <- err + return + } + + log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + port) + + app.Port = port + + errChan <- httpserver.Serve(ln) }() stop := make(chan os.Signal, 1) @@ -81,10 +104,24 @@ func (app *Application) Run() { case err := <-errChan: log.Error().Err(err).Msg("HTTP-Server failed") + + case _ = <-app.stopChan: + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + log.Info().Msg("Manually stopping HTTP-Server") + + err := httpserver.Shutdown(ctx) + + if err != nil { + log.Info().Err(err).Msg("Error while stopping the http-server") + } else { + log.Info().Msg("Manually stopped HTTP-Server") + } } for _, job := range app.Jobs { - job.Start() + job.Stop() } } diff --git a/server/push/testSink.go b/server/push/testSink.go new file mode 100644 index 0000000..1786dbe --- /dev/null +++ b/server/push/testSink.go @@ -0,0 +1,37 @@ +package push + +import ( + "blackforestbytes.com/simplecloudnotifier/models" + "context" + _ "embed" + "gogs.mikescher.com/BlackForestBytes/goext/langext" +) + +type SinkData struct { + Message models.Message + Client models.Client +} + +type TestSink struct { + data []SinkData +} + +func NewTestSink() NotificationClient { + return &TestSink{} +} + +func (d *TestSink) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) { + id, err := langext.NewHexUUID() + if err != nil { + return "", err + } + + key := "TestSink[" + id + "]" + + d.data = append(d.data, SinkData{ + Message: msg, + Client: client, + }) + + return key, nil +} diff --git a/server/swagger/swagger.json b/server/swagger/swagger.json index 3ac49d4..65f2aca 100644 --- a/server/swagger/swagger.json +++ b/server/swagger/swagger.json @@ -785,6 +785,35 @@ } } }, + "/api/sleep/{secs}": { + "post": { + "tags": [ + "Common" + ], + "summary": "Return 200 after x seconds", + "operationId": "api-common-sleep", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.Sleep.response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ginresp.apiError" + } + } + } + } + }, "/api/update.php": { "get": { "tags": [ @@ -2468,6 +2497,20 @@ } } }, + "handler.Sleep.response": { + "type": "object", + "properties": { + "duration": { + "type": "number" + }, + "end": { + "type": "string" + }, + "start": { + "type": "string" + } + } + }, "handler.Update.response": { "type": "object", "properties": { diff --git a/server/swagger/swagger.yaml b/server/swagger/swagger.yaml index 94222d7..cd7987b 100644 --- a/server/swagger/swagger.yaml +++ b/server/swagger/swagger.yaml @@ -228,6 +228,15 @@ definitions: user_key: type: string type: object + handler.Sleep.response: + properties: + duration: + type: number + end: + type: string + start: + type: string + type: object handler.Update.response: properties: is_pro: @@ -962,6 +971,25 @@ paths: summary: Return all not-acknowledged messages tags: - API-v1 + /api/sleep/{secs}: + post: + operationId: api-common-sleep + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.Sleep.response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ginresp.apiError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ginresp.apiError' + summary: Return 200 after x seconds + tags: + - Common /api/update.php: get: deprecated: true diff --git a/server/test/common_test.go b/server/test/common_test.go new file mode 100644 index 0000000..5a24aa1 --- /dev/null +++ b/server/test/common_test.go @@ -0,0 +1,103 @@ +package test + +import ( + scn "blackforestbytes.com/simplecloudnotifier" + "blackforestbytes.com/simplecloudnotifier/api" + "blackforestbytes.com/simplecloudnotifier/common/ginext" + "blackforestbytes.com/simplecloudnotifier/db" + "blackforestbytes.com/simplecloudnotifier/jobs" + "blackforestbytes.com/simplecloudnotifier/logic" + "blackforestbytes.com/simplecloudnotifier/push" + "bytes" + "encoding/json" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "io" + "net/http" + "os" + "path/filepath" + "testing" + "time" +) + +func NewSimpleWebserver(t *testing.T) *logic.Application { + + uuid, err := langext.NewHexUUID() + if err != nil { + panic(err) + } + + dbfile := filepath.Join(os.TempDir(), uuid+"sqlite3") + defer func() { + _ = os.Remove(dbfile) + }() + + conf := scn.Config{ + Namespace: "test", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "0", // simply choose a free port + DBFile: dbfile, + RequestTimeout: 500 * time.Millisecond, + ReturnRawErrors: true, + DummyFirebase: true, + } + + sqlite, err := db.NewDatabase(dbfile) + if err != nil { + panic(err) + } + + app := logic.NewApp(sqlite) + + if err := app.Migrate(); err != nil { + panic(err) + } + + ginengine := ginext.NewEngine(conf) + + router := api.NewRouter(app) + + nc := push.NewTestSink() + + jobRetry := jobs.NewDeliveryRetryJob(app) + app.Init(conf, ginengine, nc, []logic.Job{jobRetry}) + + router.Init(ginengine) + + return app +} + +func requestGet[T any](t *testing.T, baseURL string, prefix string) T { + client := http.Client{} + + req, err := http.NewRequest("GET", baseURL+prefix, bytes.NewReader([]byte{})) + if err != nil { + t.Error(err) + return *new(T) + } + + resp, err := client.Do(req) + if err != nil { + t.Error(err) + return *new(T) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != 200 { + t.Error("Statuscode != 200") + } + + respBodyBin, err := io.ReadAll(resp.Body) + if err != nil { + t.Error(err) + return *new(T) + } + + var data T + if err := json.Unmarshal(respBodyBin, &data); err != nil { + t.Error(err) + return *new(T) + } + + return data +} diff --git a/server/test/webserver_test.go b/server/test/webserver_test.go new file mode 100644 index 0000000..7cbe935 --- /dev/null +++ b/server/test/webserver_test.go @@ -0,0 +1,25 @@ +package test + +import ( + "testing" + "time" +) + +func TestWebserver(t *testing.T) { + ws := NewSimpleWebserver(t) + defer ws.Stop() + go func() { ws.Run() }() + time.Sleep(100 * time.Millisecond) +} + +func TestPing(t *testing.T) { + ws := NewSimpleWebserver(t) + defer ws.Stop() + go func() { ws.Run() }() + time.Sleep(100 * time.Millisecond) + + baseUrl := "http://127.0.0.1:" + ws.Port + + _ = requestGet[struct{}](t, baseUrl, "/api/ping") + +}