CreateUser test

This commit is contained in:
Mike Schwörer 2022-11-24 12:53:27 +01:00
parent 37e09d6532
commit 6d80638cf8
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
14 changed files with 511 additions and 79 deletions

View File

@ -60,7 +60,14 @@ clean:
rm -rf .run-data/*
git clean -fdx
go clean
go clean -testcache
fmt:
go fmt ./...
swag fmt
.PHONY: test
test:
go test ./test/...

View File

@ -38,19 +38,20 @@ func NewAPIHandler(app *logic.Application) APIHandler {
//
// @Param post_body body handler.CreateUser.body false " "
//
// @Success 200 {object} handler.sendMessageInternal.response
// @Success 200 {object} models.UserJSONWithClients
// @Failure 400 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /api/users/ [POST]
// @Router /api/users [POST]
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
type body struct {
FCMToken string `json:"fcm_token" binding:"required"`
FCMToken string `json:"fcm_token"`
ProToken *string `json:"pro_token"`
Username *string `json:"username"`
AgentModel string `json:"agent_model" binding:"required"`
AgentVersion string `json:"agent_version" binding:"required"`
ClientType string `json:"client_type" binding:"required"`
AgentModel string `json:"agent_model"`
AgentVersion string `json:"agent_version"`
ClientType string `json:"client_type"`
NoClient bool `json:"no_client"`
}
var b body
@ -61,12 +62,23 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
defer ctx.Cancel()
var clientType models.ClientType
if b.ClientType == string(models.ClientTypeAndroid) {
clientType = models.ClientTypeAndroid
} else if b.ClientType == string(models.ClientTypeIOS) {
clientType = models.ClientTypeIOS
} else {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil)
if !b.NoClient {
if b.FCMToken == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing FCMToken", nil)
}
if b.AgentVersion == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing AgentVersion", nil)
}
if b.ClientType == "" {
return ginresp.APIError(g, 400, apierr.INVALID_CLIENTTYPE, "Missing ClientType", nil)
}
if b.ClientType == string(models.ClientTypeAndroid) {
clientType = models.ClientTypeAndroid
} else if b.ClientType == string(models.ClientTypeIOS) {
clientType = models.ClientTypeIOS
} else {
return ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Invalid ClientType", nil)
}
}
if b.ProToken != nil {
@ -106,12 +118,17 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
}
_, err = h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
if b.NoClient {
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0))))
} else {
client, err := h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients([]models.Client{client})))
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON()))
}
// GetUser swaggerdoc
@ -409,7 +426,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create client in db", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, client.JSON()))

View File

@ -5,9 +5,11 @@ import (
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
"blackforestbytes.com/simplecloudnotifier/logic"
"bytes"
"context"
"errors"
"github.com/gin-gonic/gin"
sqlite3 "github.com/mattn/go-sqlite3"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"net/http"
"time"
@ -74,7 +76,7 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
// @Success 200 {object} handler.DatabaseTest.response
// @Failure 500 {object} ginresp.apiError
//
// @Router /api/db-test [get]
// @Router /api/db-test [post]
func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
type response struct {
Success bool `json:"success"`
@ -113,6 +115,9 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
Status string `json:"status"`
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, libVersionNumber, _ := sqlite3.Version()
if libVersionNumber < 3039000 {
@ -124,6 +129,28 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
return ginresp.InternalError(err)
}
uuidKey, _ := langext.NewHexUUID()
uuidWrite, _ := langext.NewHexUUID()
err = h.app.Database.WriteMetaString(ctx, uuidKey, uuidWrite)
if err != nil {
return ginresp.InternalError(err)
}
uuidRead, err := h.app.Database.ReadMetaString(ctx, uuidKey)
if err != nil {
return ginresp.InternalError(err)
}
if uuidRead == nil || uuidWrite != *uuidRead {
return ginresp.InternalError(errors.New("writing into DB was not consistent"))
}
err = h.app.Database.DeleteMeta(ctx, uuidKey)
if err != nil {
return ginresp.InternalError(err)
}
return ginresp.JSON(http.StatusOK, response{Status: "ok"})
}

View File

@ -118,7 +118,7 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
_, err = h.database.CreateClient(ctx, user.UserID, models.ClientTypeAndroid, *data.FCMToken, "compat", "compat")
if err != nil {
return ginresp.CompatAPIError(0, "Failed to create user in db")
return ginresp.CompatAPIError(0, "Failed to create client in db")
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, response{

View File

@ -35,6 +35,11 @@ func (db *Database) Migrate(ctx context.Context) error {
return err
}
err = db.WriteMetaInt(ctx, "schema", 3)
if err != nil {
return err
}
return nil
} else if currschema == 1 {
@ -49,34 +54,6 @@ func (db *Database) Migrate(ctx context.Context) error {
}
func (db *Database) ReadSchema(ctx context.Context) (int, error) {
r1, err := db.db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'")
if err != nil {
return 0, err
}
if !r1.Next() {
return 0, nil
}
r2, err := db.db.QueryContext(ctx, "SELECT value_int FROM meta WHERE meta_key='schema'")
if err != nil {
return 0, err
}
if !r2.Next() {
return 0, errors.New("no schema entry in meta table")
}
var dbschema int
err = r2.Scan(&dbschema)
if err != nil {
return 0, err
}
return dbschema, nil
}
func (db *Database) Ping() error {
return db.db.Ping()
}

159
server/db/meta.go Normal file
View File

@ -0,0 +1,159 @@
package db
import (
"context"
"errors"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
)
func (db *Database) ReadSchema(ctx context.Context) (int, error) {
r1, err := db.db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'")
if err != nil {
return 0, err
}
if !r1.Next() {
return 0, nil
}
r2, err := db.db.QueryContext(ctx, "SELECT value_int FROM meta WHERE meta_key='schema'")
if err != nil {
return 0, err
}
if !r2.Next() {
return 0, errors.New("no schema entry in meta table")
}
var dbschema int
err = r2.Scan(&dbschema)
if err != nil {
return 0, err
}
return dbschema, nil
}
func (db *Database) WriteMetaString(ctx context.Context, key string, value string) error {
_, err := db.db.ExecContext(ctx, "INSERT INTO meta (meta_key, value_txt) VALUES (?, ?) ON CONFLICT(meta_key) DO UPDATE SET value_txt = ?",
key,
value,
value)
if err != nil {
return err
}
return nil
}
func (db *Database) WriteMetaInt(ctx context.Context, key string, value int64) error {
_, err := db.db.ExecContext(ctx, "INSERT INTO meta (meta_key, value_int) VALUES (?, ?) ON CONFLICT(meta_key) DO UPDATE SET value_int = ?",
key,
value,
value)
if err != nil {
return err
}
return nil
}
func (db *Database) WriteMetaReal(ctx context.Context, key string, value float64) error {
_, err := db.db.ExecContext(ctx, "INSERT INTO meta (meta_key, value_real) VALUES (?, ?) ON CONFLICT(meta_key) DO UPDATE SET value_real = ?",
key,
value,
value)
if err != nil {
return err
}
return nil
}
func (db *Database) WriteMetaBlob(ctx context.Context, key string, value []byte) error {
_, err := db.db.ExecContext(ctx, "INSERT INTO meta (meta_key, value_blob) VALUES (?, ?) ON CONFLICT(meta_key) DO UPDATE SET value_blob = ?",
key,
value,
value)
if err != nil {
return err
}
return nil
}
func (db *Database) ReadMetaString(ctx context.Context, key string) (*string, error) {
r2, err := db.db.QueryContext(ctx, "SELECT value_txt FROM meta WHERE meta_key=?", key)
if err != nil {
return nil, err
}
if !r2.Next() {
return nil, errors.New("no matching entry in meta table")
}
var value string
err = r2.Scan(&value)
if err != nil {
return nil, err
}
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaInt(ctx context.Context, key string) (*int64, error) {
r2, err := db.db.QueryContext(ctx, "SELECT value_int FROM meta WHERE meta_key=?", key)
if err != nil {
return nil, err
}
if !r2.Next() {
return nil, errors.New("no matching entry in meta table")
}
var value int64
err = r2.Scan(&value)
if err != nil {
return nil, err
}
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaReal(ctx context.Context, key string) (*float64, error) {
r2, err := db.db.QueryContext(ctx, "SELECT value_real FROM meta WHERE meta_key=?", key)
if err != nil {
return nil, err
}
if !r2.Next() {
return nil, errors.New("no matching entry in meta table")
}
var value float64
err = r2.Scan(&value)
if err != nil {
return nil, err
}
return langext.Ptr(value), nil
}
func (db *Database) ReadMetaBlob(ctx context.Context, key string) (*[]byte, error) {
r2, err := db.db.QueryContext(ctx, "SELECT value_blob FROM meta WHERE meta_key=?", key)
if err != nil {
return nil, err
}
if !r2.Next() {
return nil, errors.New("no matching entry in meta table")
}
var value []byte
err = r2.Scan(&value)
if err != nil {
return nil, err
}
return langext.Ptr(value), nil
}
func (db *Database) DeleteMeta(ctx context.Context, key string) error {
_, err := db.db.ExecContext(ctx, "DELETE FROM meta WHERE meta_key = ?", key)
if err != nil {
return err
}
return nil
}

View File

@ -125,4 +125,6 @@ CREATE TABLE `meta`
PRIMARY KEY (meta_key)
);
INSERT INTO meta (meta_key, value_int) VALUES ('schema', 3)

View File

@ -167,7 +167,7 @@ func (app *Application) StartRequest(g *gin.Context, uri any, query any, body an
}
}
if body != nil && g.Request.Header.Get("Content-Type") == "application/javascript" {
if body != nil && g.Request.Header.Get("Content-Type") == "application/json" {
if err := g.ShouldBindJSON(body); err != nil {
return nil, langext.Ptr(ginresp.APIError(g, 400, apierr.BINDFAIL_BODY_PARAM, "Failed to read body", err))
}

View File

@ -42,6 +42,13 @@ func (u User) JSON() UserJSON {
}
}
func (u User) JSONWithClients(clients []Client) UserJSONWithClients {
return UserJSONWithClients{
UserJSON: u.JSON(),
Clients: langext.ArrMap(clients, func(v Client) ClientJSON { return v.JSON() }),
}
}
func (u User) MaxContentLength() int {
if u.IsPro {
return 16384
@ -99,6 +106,11 @@ type UserJSON struct {
DefaultChannel string `json:"default_channel"`
}
type UserJSONWithClients struct {
UserJSON
Clients []ClientJSON `json:"clients"`
}
type UserDB struct {
UserID UserID `db:"user_id"`
Username *string `db:"username"`

View File

@ -1034,7 +1034,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.sendMessageInternal.response"
"$ref": "#/definitions/models.UserJSONWithClients"
}
},
"400": {
@ -2945,6 +2945,56 @@
"type": "string"
}
}
},
"models.UserJSONWithClients": {
"type": "object",
"properties": {
"admin_key": {
"type": "string"
},
"clients": {
"type": "array",
"items": {
"$ref": "#/definitions/models.ClientJSON"
}
},
"default_channel": {
"type": "string"
},
"is_pro": {
"type": "boolean"
},
"messages_sent": {
"type": "integer"
},
"quota_used": {
"type": "integer"
},
"quota_used_day": {
"type": "string"
},
"read_key": {
"type": "string"
},
"send_key": {
"type": "string"
},
"timestamp_created": {
"type": "string"
},
"timestamp_last_read": {
"type": "string"
},
"timestamp_last_sent": {
"type": "string"
},
"user_id": {
"type": "integer"
},
"username": {
"type": "string"
}
}
}
},
"tags": [

View File

@ -442,6 +442,39 @@ definitions:
username:
type: string
type: object
models.UserJSONWithClients:
properties:
admin_key:
type: string
clients:
items:
$ref: '#/definitions/models.ClientJSON'
type: array
default_channel:
type: string
is_pro:
type: boolean
messages_sent:
type: integer
quota_used:
type: integer
quota_used_day:
type: string
read_key:
type: string
send_key:
type: string
timestamp_created:
type: string
timestamp_last_read:
type: string
timestamp_last_sent:
type: string
user_id:
type: integer
username:
type: string
type: object
host: scn.blackforestbytes.com
info:
contact: {}
@ -1142,7 +1175,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handler.sendMessageInternal.response'
$ref: '#/definitions/models.UserJSONWithClients'
"400":
description: Bad Request
schema:

View File

@ -10,26 +10,65 @@ import (
"blackforestbytes.com/simplecloudnotifier/push"
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"io"
"net/http"
"os"
"path/filepath"
"testing"
"time"
)
func NewSimpleWebserver(t *testing.T) *logic.Application {
type Void = struct{}
uuid, err := langext.NewHexUUID()
func NewSimpleWebserver() (*logic.Application, func()) {
cw := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "2006-01-02 15:04:05 Z07:00",
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
multi := zerolog.MultiLevelWriter(cw)
logger := zerolog.New(multi).With().
Timestamp().
Caller().
Logger()
log.Logger = logger
gin.SetMode(gin.TestMode)
zerolog.SetGlobalLevel(zerolog.DebugLevel)
uuid1, _ := langext.NewHexUUID()
uuid2, _ := langext.NewHexUUID()
dbdir := filepath.Join(os.TempDir(), uuid1)
dbfile := filepath.Join(dbdir, uuid2+".sqlite3")
err := os.MkdirAll(dbdir, os.ModePerm)
if err != nil {
panic(err)
}
dbfile := filepath.Join(os.TempDir(), uuid+"sqlite3")
defer func() {
_ = os.Remove(dbfile)
}()
f, err := os.Create(dbfile)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
err = os.Chmod(dbfile, 0777)
if err != nil {
panic(err)
}
//dbfile := "/home/mike/Code/private/SimpleCloudNotifier/server/.run-data/db_test.sqlite3"
fmt.Println("DatabaseFile: " + dbfile)
conf := scn.Config{
Namespace: "test",
@ -64,39 +103,94 @@ func NewSimpleWebserver(t *testing.T) *logic.Application {
router.Init(ginengine)
return app
return app, func() { app.Stop(); _ = os.Remove(dbfile) }
}
func requestGet[T any](t *testing.T, baseURL string, prefix string) T {
func requestGet[TResult any](baseURL string, prefix string) TResult {
return requestAny[TResult]("", "GET", baseURL, prefix, nil)
}
func requestAuthGet[TResult any](akey string, baseURL string, prefix string) TResult {
return requestAny[TResult](akey, "GET", baseURL, prefix, nil)
}
func requestPost[TResult any](baseURL string, prefix string, body any) TResult {
return requestAny[TResult]("", "POST", baseURL, prefix, body)
}
func requestAuthPost[TResult any](akey string, baseURL string, prefix string, body any) TResult {
return requestAny[TResult](akey, "POST", baseURL, prefix, body)
}
func requestPut[TResult any](baseURL string, prefix string, body any) TResult {
return requestAny[TResult]("", "PUT", baseURL, prefix, body)
}
func requestAuthPUT[TResult any](akey string, baseURL string, prefix string, body any) TResult {
return requestAny[TResult](akey, "PUT", baseURL, prefix, body)
}
func requestPatch[TResult any](baseURL string, prefix string, body any) TResult {
return requestAny[TResult]("", "PATCH", baseURL, prefix, body)
}
func requestAuthPatch[TResult any](akey string, baseURL string, prefix string, body any) TResult {
return requestAny[TResult](akey, "PATCH", baseURL, prefix, body)
}
func requestDelete[TResult any](baseURL string, prefix string, body any) TResult {
return requestAny[TResult]("", "DELETE", baseURL, prefix, body)
}
func requestAuthDelete[TResult any](akey string, baseURL string, prefix string, body any) TResult {
return requestAny[TResult](akey, "DELETE", baseURL, prefix, body)
}
func requestAny[TResult any](akey string, method string, baseURL string, prefix string, body any) TResult {
client := http.Client{}
req, err := http.NewRequest("GET", baseURL+prefix, bytes.NewReader([]byte{}))
bytesbody := make([]byte, 0)
if body != nil {
bjson, err := json.Marshal(body)
if err != nil {
panic(err)
}
bytesbody = bjson
}
req, err := http.NewRequest(method, baseURL+prefix, bytes.NewReader(bytesbody))
if err != nil {
t.Error(err)
return *new(T)
panic(err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if akey != "" {
req.Header.Set("Authorization", "SCN "+akey)
}
resp, err := client.Do(req)
if err != nil {
t.Error(err)
return *new(T)
panic(err)
}
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)
panic(err)
}
var data T
if resp.StatusCode != 200 {
fmt.Println("Request: " + method + " :: " + baseURL + prefix)
fmt.Println(string(respBodyBin))
panic("Statuscode != 200")
}
var data TResult
if err := json.Unmarshal(respBodyBin, &data); err != nil {
t.Error(err)
return *new(T)
panic(err)
}
return data

29
server/test/user_test.go Normal file
View File

@ -0,0 +1,29 @@
package test
import (
"fmt"
"github.com/gin-gonic/gin"
"testing"
"time"
)
func TestCreateUserNoClient(t *testing.T) {
ws, stop := NewSimpleWebserver()
defer stop()
go func() { ws.Run() }()
time.Sleep(100 * time.Millisecond)
baseUrl := "http://127.0.0.1:" + ws.Port
res := requestPost[gin.H](baseUrl, "/api/users", gin.H{
"no_client": true,
})
uid := fmt.Sprintf("%v", res["user_id"])
admintok := res["admin_key"].(string)
fmt.Printf("uid := %s\n", uid)
fmt.Printf("admin_key := %s\n", admintok)
requestAuthGet[Void](admintok, baseUrl, "/api/users/"+uid)
}

View File

@ -6,20 +6,45 @@ import (
)
func TestWebserver(t *testing.T) {
ws := NewSimpleWebserver(t)
defer ws.Stop()
ws, stop := NewSimpleWebserver()
defer stop()
go func() { ws.Run() }()
time.Sleep(100 * time.Millisecond)
}
func TestPing(t *testing.T) {
ws := NewSimpleWebserver(t)
defer ws.Stop()
ws, stop := NewSimpleWebserver()
defer stop()
go func() { ws.Run() }()
time.Sleep(100 * time.Millisecond)
baseUrl := "http://127.0.0.1:" + ws.Port
_ = requestGet[struct{}](t, baseUrl, "/api/ping")
_ = requestGet[Void](baseUrl, "/api/ping")
_ = requestPut[Void](baseUrl, "/api/ping", nil)
_ = requestPost[Void](baseUrl, "/api/ping", nil)
_ = requestPatch[Void](baseUrl, "/api/ping", nil)
_ = requestDelete[Void](baseUrl, "/api/ping", nil)
}
func TestMongo(t *testing.T) {
ws, stop := NewSimpleWebserver()
defer stop()
go func() { ws.Run() }()
time.Sleep(100 * time.Millisecond)
baseUrl := "http://127.0.0.1:" + ws.Port
_ = requestPost[Void](baseUrl, "/api/db-test", nil)
}
func TestHealth(t *testing.T) {
ws, stop := NewSimpleWebserver()
defer stop()
go func() { ws.Run() }()
time.Sleep(100 * time.Millisecond)
baseUrl := "http://127.0.0.1:" + ws.Port
_ = requestGet[Void](baseUrl, "/api/health")
}