tests (boilerplate)

This commit is contained in:
Mike Schwörer 2022-11-23 20:21:49 +01:00
parent 1bc847cdc9
commit 8ea3fdcfef
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
10 changed files with 323 additions and 8 deletions

View File

@ -1,13 +1,16 @@
package handler package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/common/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/logic"
"bytes" "bytes"
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
sqlite3 "github.com/mattn/go-sqlite3" sqlite3 "github.com/mattn/go-sqlite3"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"net/http" "net/http"
"time"
) )
type CommonHandler struct { type CommonHandler struct {
@ -106,7 +109,7 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
// @Failure 500 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError
// //
// @Router /api/health [get] // @Router /api/health [get]
func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse { func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
type response struct { type response struct {
Status string `json:"status"` Status string `json:"status"`
} }
@ -125,6 +128,45 @@ func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse {
return ginresp.JSON(http.StatusOK, response{Status: "ok"}) 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 { func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse {
return ginresp.JSON(http.StatusNotFound, gin.H{ return ginresp.JSON(http.StatusNotFound, gin.H{
"": "================ ROUTE NOT FOUND ================", "": "================ ROUTE NOT FOUND ================",

View File

@ -53,6 +53,7 @@ func (r *Router) Init(e *gin.Engine) {
commonAPI.Any("/ping", ginresp.Wrap(r.commonHandler.Ping)) commonAPI.Any("/ping", ginresp.Wrap(r.commonHandler.Ping))
commonAPI.POST("/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest)) commonAPI.POST("/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest))
commonAPI.GET("/health", ginresp.Wrap(r.commonHandler.Health)) commonAPI.GET("/health", ginresp.Wrap(r.commonHandler.Health))
commonAPI.POST("/sleep/:secs", ginresp.Wrap(r.commonHandler.Sleep))
} }
// ================ Swagger ================ // ================ Swagger ================

View File

@ -20,7 +20,7 @@ func main() {
log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace)) 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 { if err != nil {
panic(err) panic(err)
} }

View File

@ -1,7 +1,6 @@
package db package db
import ( import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/db/schema" "blackforestbytes.com/simplecloudnotifier/db/schema"
"context" "context"
"database/sql" "database/sql"
@ -15,8 +14,8 @@ type Database struct {
db *sql.DB db *sql.DB
} }
func NewDatabase(conf scn.Config) (*Database, error) { func NewDatabase(filename string) (*Database, error) {
db, err := sql.Open("sqlite3", conf.DBFile) db, err := sql.Open("sqlite3", filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -29,11 +29,14 @@ type Application struct {
Database *db.Database Database *db.Database
Firebase push.NotificationClient Firebase push.NotificationClient
Jobs []Job Jobs []Job
stopChan chan bool
Port string
} }
func NewApp(db *db.Database) *Application { func NewApp(db *db.Database) *Application {
return &Application{ return &Application{
Database: db, 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 app.Jobs = jobs
} }
func (app *Application) Stop() {
app.stopChan <- true
}
func (app *Application) Run() { func (app *Application) Run() {
httpserver := &http.Server{ httpserver := &http.Server{
Addr: net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort), Addr: net.JoinHostPort(app.Config.ServerIP, app.Config.ServerPort),
@ -53,8 +60,24 @@ func (app *Application) Run() {
errChan := make(chan error) errChan := make(chan error)
go func() { 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) stop := make(chan os.Signal, 1)
@ -81,10 +104,24 @@ func (app *Application) Run() {
case err := <-errChan: case err := <-errChan:
log.Error().Err(err).Msg("HTTP-Server failed") 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 { for _, job := range app.Jobs {
job.Start() job.Stop()
} }
} }

37
server/push/testSink.go Normal file
View File

@ -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
}

View File

@ -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": { "/api/update.php": {
"get": { "get": {
"tags": [ "tags": [
@ -2468,6 +2497,20 @@
} }
} }
}, },
"handler.Sleep.response": {
"type": "object",
"properties": {
"duration": {
"type": "number"
},
"end": {
"type": "string"
},
"start": {
"type": "string"
}
}
},
"handler.Update.response": { "handler.Update.response": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -228,6 +228,15 @@ definitions:
user_key: user_key:
type: string type: string
type: object type: object
handler.Sleep.response:
properties:
duration:
type: number
end:
type: string
start:
type: string
type: object
handler.Update.response: handler.Update.response:
properties: properties:
is_pro: is_pro:
@ -962,6 +971,25 @@ paths:
summary: Return all not-acknowledged messages summary: Return all not-acknowledged messages
tags: tags:
- API-v1 - 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: /api/update.php:
get: get:
deprecated: true deprecated: true

103
server/test/common_test.go Normal file
View File

@ -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
}

View File

@ -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")
}