diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 70ccdb7..1c8b272 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -227,7 +227,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse { username = nil } - err := h.database.UpdateUserUsername(ctx, u.UserID, b.Username) + err := h.database.UpdateUserUsername(ctx, u.UserID, username) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update user", err) } diff --git a/server/test/clients_test.go b/server/test/clients_test.go index ff4f080..ba2a841 100644 --- a/server/test/clients_test.go +++ b/server/test/clients_test.go @@ -1,18 +1,19 @@ package test import ( + tt "blackforestbytes.com/simplecloudnotifier/test/util" "fmt" "github.com/gin-gonic/gin" "testing" ) func TestGetClient(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ "agent_model": "DUMMY_PHONE", "agent_version": "4X", "client_type": "ANDROID", @@ -21,49 +22,49 @@ func TestGetClient(t *testing.T) { uid := fmt.Sprintf("%v", r0["user_id"]) - assertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) + tt.AssertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) admintok := r0["admin_key"].(string) fmt.Printf("uid := %s\n", uid) fmt.Printf("admin_key := %s\n", admintok) - r1 := requestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) - assertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) - assertEqual(t, "admin_key", admintok, r1["admin_key"]) - assertEqual(t, "username", nil, r1["username"]) + tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) + tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"]) + tt.AssertEqual(t, "username", nil, r1["username"]) type rt2 struct { Clients []gin.H `json:"clients"` } - r2 := requestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + r2 := tt.RequestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") - assertEqual(t, "len(clients)", 1, len(r2.Clients)) + tt.AssertEqual(t, "len(clients)", 1, len(r2.Clients)) c0 := r2.Clients[0] - assertEqual(t, "agent_model", "DUMMY_PHONE", c0["agent_model"]) - assertEqual(t, "agent_version", "4X", c0["agent_version"]) - assertEqual(t, "fcm_token", "DUMMY_FCM", c0["fcm_token"]) - assertEqual(t, "client_type", "ANDROID", c0["type"]) - assertEqual(t, "user_id", uid, fmt.Sprintf("%v", c0["user_id"])) + tt.AssertEqual(t, "agent_model", "DUMMY_PHONE", c0["agent_model"]) + tt.AssertEqual(t, "agent_version", "4X", c0["agent_version"]) + tt.AssertEqual(t, "fcm_token", "DUMMY_FCM", c0["fcm_token"]) + tt.AssertEqual(t, "client_type", "ANDROID", c0["type"]) + tt.AssertEqual(t, "user_id", uid, fmt.Sprintf("%v", c0["user_id"])) cid := fmt.Sprintf("%v", c0["client_id"]) - r3 := requestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients/"+cid) + r3 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients/"+cid) - assertJsonMapEqual(t, "client", r3, c0) + tt.AssertJsonMapEqual(t, "client", r3, c0) } func TestCreateAndDeleteClient(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ "agent_model": "DUMMY_PHONE", "agent_version": "4X", "client_type": "ANDROID", @@ -72,14 +73,14 @@ func TestCreateAndDeleteClient(t *testing.T) { uid := fmt.Sprintf("%v", r0["user_id"]) - assertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) + tt.AssertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) admintok := r0["admin_key"].(string) fmt.Printf("uid := %s\n", uid) fmt.Printf("admin_key := %s\n", admintok) - r2 := requestAuthPost[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients", gin.H{ + r2 := tt.RequestAuthPost[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients", gin.H{ "agent_model": "DUMMY_PHONE_2", "agent_version": "99X", "client_type": "IOS", @@ -92,23 +93,23 @@ func TestCreateAndDeleteClient(t *testing.T) { Clients []gin.H `json:"clients"` } - r3 := requestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") - assertEqual(t, "len(clients)", 2, len(r3.Clients)) + r3 := tt.RequestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + tt.AssertEqual(t, "len(clients)", 2, len(r3.Clients)) - r4 := requestAuthDelete[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients/"+cid2, nil) - assertEqual(t, "client_id", cid2, fmt.Sprintf("%v", r4["client_id"])) + r4 := tt.RequestAuthDelete[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients/"+cid2, nil) + tt.AssertEqual(t, "client_id", cid2, fmt.Sprintf("%v", r4["client_id"])) - r5 := requestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") - assertEqual(t, "len(clients)", 1, len(r5.Clients)) + r5 := tt.RequestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + tt.AssertEqual(t, "len(clients)", 1, len(r5.Clients)) } func TestReuseFCM(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ "agent_model": "DUMMY_PHONE", "agent_version": "4X", "client_type": "ANDROID", @@ -117,7 +118,7 @@ func TestReuseFCM(t *testing.T) { uid := fmt.Sprintf("%v", r0["user_id"]) - assertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) + tt.AssertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) admintok := r0["admin_key"].(string) @@ -128,11 +129,11 @@ func TestReuseFCM(t *testing.T) { Clients []gin.H `json:"clients"` } - r1 := requestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + r1 := tt.RequestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") - assertEqual(t, "len(clients)", 1, len(r1.Clients)) + tt.AssertEqual(t, "len(clients)", 1, len(r1.Clients)) - r2 := requestAuthPost[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients", gin.H{ + r2 := tt.RequestAuthPost[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients", gin.H{ "agent_model": "DUMMY_PHONE_2", "agent_version": "99X", "client_type": "IOS", @@ -145,8 +146,8 @@ func TestReuseFCM(t *testing.T) { Clients []gin.H `json:"clients"` } - r3 := requestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") - assertEqual(t, "len(clients)", 1, len(r3.Clients)) + r3 := tt.RequestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + tt.AssertEqual(t, "len(clients)", 1, len(r3.Clients)) - assertEqual(t, "clients->client_id", cid2, fmt.Sprintf("%v", r3.Clients[0]["client_id"])) + tt.AssertEqual(t, "clients->client_id", cid2, fmt.Sprintf("%v", r3.Clients[0]["client_id"])) } diff --git a/server/test/common_test.go b/server/test/common_test.go deleted file mode 100644 index 8c02976..0000000 --- a/server/test/common_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package test - -import ( - scn "blackforestbytes.com/simplecloudnotifier" - "blackforestbytes.com/simplecloudnotifier/api" - "blackforestbytes.com/simplecloudnotifier/common/ginext" - "blackforestbytes.com/simplecloudnotifier/db" - "blackforestbytes.com/simplecloudnotifier/google" - "blackforestbytes.com/simplecloudnotifier/jobs" - "blackforestbytes.com/simplecloudnotifier/logic" - "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" - "runtime/debug" - "strings" - "testing" - "time" -) - -type Void = struct{} - -func StartSimpleWebserver(t *testing.T) (*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) - - uuid2, _ := langext.NewHexUUID() - dbdir := t.TempDir() - dbfile := filepath.Join(dbdir, uuid2+".sqlite3") - - err := os.MkdirAll(dbdir, os.ModePerm) - if err != nil { - testFailErr(t, err) - } - - f, err := os.Create(dbfile) - if err != nil { - testFailErr(t, err) - } - err = f.Close() - if err != nil { - testFailErr(t, err) - } - - err = os.Chmod(dbfile, 0777) - if err != nil { - testFailErr(t, err) - } - - fmt.Println("DatabaseFile: " + 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 { - testFailErr(t, err) - } - - app := logic.NewApp(sqlite) - - if err := app.Migrate(); err != nil { - testFailErr(t, err) - } - - ginengine := ginext.NewEngine(conf) - - router := api.NewRouter(app) - - nc := push.NewTestSink() - - apc := google.NewDummy() - - jobRetry := jobs.NewDeliveryRetryJob(app) - app.Init(conf, ginengine, nc, apc, []logic.Job{jobRetry}) - - router.Init(ginengine) - - stop := func() { app.Stop(); _ = os.Remove(dbfile) } - go func() { app.Run() }() - time.Sleep(100 * time.Millisecond) - return app, stop -} - -func requestGet[TResult any](t *testing.T, baseURL string, prefix string) TResult { - return requestAny[TResult](t, "", "GET", baseURL, prefix, nil) -} - -func requestAuthGet[TResult any](t *testing.T, akey string, baseURL string, prefix string) TResult { - return requestAny[TResult](t, akey, "GET", baseURL, prefix, nil) -} - -func requestPost[TResult any](t *testing.T, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, "", "POST", baseURL, prefix, body) -} - -func requestAuthPost[TResult any](t *testing.T, akey string, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, akey, "POST", baseURL, prefix, body) -} - -func requestPut[TResult any](t *testing.T, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, "", "PUT", baseURL, prefix, body) -} - -func requestAuthPUT[TResult any](t *testing.T, akey string, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, akey, "PUT", baseURL, prefix, body) -} - -func requestPatch[TResult any](t *testing.T, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, "", "PATCH", baseURL, prefix, body) -} - -func requestAuthPatch[TResult any](t *testing.T, akey string, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, akey, "PATCH", baseURL, prefix, body) -} - -func requestDelete[TResult any](t *testing.T, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, "", "DELETE", baseURL, prefix, body) -} - -func requestAuthDelete[TResult any](t *testing.T, akey string, baseURL string, prefix string, body any) TResult { - return requestAny[TResult](t, akey, "DELETE", baseURL, prefix, body) -} - -func requestAny[TResult any](t *testing.T, akey string, method string, baseURL string, prefix string, body any) TResult { - client := http.Client{} - - bytesbody := make([]byte, 0) - if body != nil { - bjson, err := json.Marshal(body) - if err != nil { - testFailErr(t, err) - } - bytesbody = bjson - } - - req, err := http.NewRequest(method, baseURL+prefix, bytes.NewReader(bytesbody)) - if err != nil { - testFailErr(t, 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 { - testFailErr(t, err) - } - defer func() { _ = resp.Body.Close() }() - - respBodyBin, err := io.ReadAll(resp.Body) - if err != nil { - testFailErr(t, err) - } - - if resp.StatusCode != 200 { - fmt.Println("Request: " + method + " :: " + baseURL + prefix) - fmt.Println(string(respBodyBin)) - testFail(t, "Statuscode != 200") - } - - var data TResult - if err := json.Unmarshal(respBodyBin, &data); err != nil { - testFailErr(t, err) - } - - return data -} - -func assertJsonMapEqual(t *testing.T, key string, expected map[string]any, actual map[string]any) { - mkeys := make(map[string]string) - for k := range expected { - mkeys[k] = k - } - for k := range actual { - mkeys[k] = k - } - - for mapkey := range mkeys { - - if _, ok := expected[mapkey]; !ok { - testFailFmt(t, "Missing Key expected['%s'] ( assertJsonMapEqual[%s] )", mapkey, key) - } - if _, ok := actual[mapkey]; !ok { - testFailFmt(t, "Missing Key actual['%s'] ( assertJsonMapEqual[%s] )", mapkey, key) - } - - assertEqual(t, key+"."+mapkey, expected[mapkey], actual[mapkey]) - } - -} - -func assertEqual(t *testing.T, key string, expected any, actual any) { - if expected != actual { - t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual) - - str1 := fmt.Sprintf("%v", expected) - str2 := fmt.Sprintf("%v", actual) - - if strings.Contains(str1, "\n") { - t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected) - } else { - t.Errorf("Actual := \"%v\"\n", expected) - } - - if strings.Contains(str2, "\n") { - t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual) - } else { - t.Errorf("Expected := \"%v\"\n", actual) - } - - t.Error(debug.Stack()) - - t.FailNow() - } -} - -func testFail(t *testing.T, msg string) { - t.Error(msg) - t.FailNow() -} - -func testFailFmt(t *testing.T, format string, args ...any) { - t.Errorf(format, args...) - t.FailNow() -} - -func testFailErr(t *testing.T, e error) { - t.Error(fmt.Sprintf("Failed with error:\n%s\n\nError:\n%+v\n\nTrace:\n%s", e.Error(), e, string(debug.Stack()))) - t.FailNow() -} diff --git a/server/test/user_test.go b/server/test/user_test.go index 5bb0c6f..9639385 100644 --- a/server/test/user_test.go +++ b/server/test/user_test.go @@ -1,42 +1,41 @@ package test import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" + tt "blackforestbytes.com/simplecloudnotifier/test/util" "fmt" "github.com/gin-gonic/gin" "testing" ) func TestCreateUserNoClient(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ "no_client": true, }) - assertEqual(t, "len(clients)", 0, len(r0["clients"].([]any))) + tt.AssertEqual(t, "len(clients)", 0, len(r0["clients"].([]any))) uid := fmt.Sprintf("%v", r0["user_id"]) admintok := r0["admin_key"].(string) - fmt.Printf("uid := %s\n", uid) - fmt.Printf("admin_key := %s\n", admintok) + r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) - r1 := requestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) - - assertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) - assertEqual(t, "admin_key", admintok, r1["admin_key"]) + tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) + tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"]) } func TestCreateUserDummyClient(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ "agent_model": "DUMMY_PHONE", "agent_version": "4X", "client_type": "ANDROID", @@ -45,42 +44,39 @@ func TestCreateUserDummyClient(t *testing.T) { uid := fmt.Sprintf("%v", r0["user_id"]) - assertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) + tt.AssertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) admintok := r0["admin_key"].(string) - fmt.Printf("uid := %s\n", uid) - fmt.Printf("admin_key := %s\n", admintok) + r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) - r1 := requestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) - - assertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) - assertEqual(t, "admin_key", admintok, r1["admin_key"]) - assertEqual(t, "username", nil, r1["username"]) + tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) + tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"]) + tt.AssertEqual(t, "username", nil, r1["username"]) type rt2 struct { Clients []gin.H `json:"clients"` } - r2 := requestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + r2 := tt.RequestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") - assertEqual(t, "len(clients)", 1, len(r2.Clients)) + tt.AssertEqual(t, "len(clients)", 1, len(r2.Clients)) c0 := r2.Clients[0] - assertEqual(t, "agent_model", "DUMMY_PHONE", c0["agent_model"]) - assertEqual(t, "agent_version", "4X", c0["agent_version"]) - assertEqual(t, "fcm_token", "DUMMY_FCM", c0["fcm_token"]) - assertEqual(t, "client_type", "ANDROID", c0["type"]) + tt.AssertEqual(t, "agent_model", "DUMMY_PHONE", c0["agent_model"]) + tt.AssertEqual(t, "agent_version", "4X", c0["agent_version"]) + tt.AssertEqual(t, "fcm_token", "DUMMY_FCM", c0["fcm_token"]) + tt.AssertEqual(t, "client_type", "ANDROID", c0["type"]) } func TestCreateUserWithUsername(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ "agent_model": "DUMMY_PHONE", "agent_version": "4X", "client_type": "ANDROID", @@ -88,18 +84,138 @@ func TestCreateUserWithUsername(t *testing.T) { "username": "my_user", }) - assertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) + tt.AssertEqual(t, "len(clients)", 1, len(r0["clients"].([]any))) uid := fmt.Sprintf("%v", r0["user_id"]) admintok := r0["admin_key"].(string) - fmt.Printf("uid := %s\n", uid) - fmt.Printf("admin_key := %s\n", admintok) + r1 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + + tt.AssertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) + tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"]) + tt.AssertEqual(t, "username", "my_user", r1["username"]) +} + +func TestUpdateUsername(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + "agent_model": "DUMMY_PHONE", + "agent_version": "4X", + "client_type": "ANDROID", + "fcm_token": "DUMMY_FCM", + }) + tt.AssertEqual(t, "username", nil, r0["username"]) + + uid := fmt.Sprintf("%v", r0["user_id"]) + admintok := r0["admin_key"].(string) + + r1 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{"username": "my_user_001"}) + tt.AssertEqual(t, "username", "my_user_001", r1["username"]) + + r2 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + tt.AssertEqual(t, "username", "my_user_001", r2["username"]) + + r3 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{"username": "my_user_002"}) + tt.AssertEqual(t, "username", "my_user_002", r3["username"]) + + r4 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + tt.AssertEqual(t, "username", "my_user_002", r4["username"]) + + r5 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{"username": ""}) + tt.AssertEqual(t, "username", nil, r5["username"]) + + r6 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + tt.AssertEqual(t, "username", nil, r6["username"]) +} + +func TestRecreateKeys(t *testing.T) { + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + "agent_model": "DUMMY_PHONE", + "agent_version": "4X", + "client_type": "ANDROID", + "fcm_token": "DUMMY_FCM", + }) + tt.AssertEqual(t, "username", nil, r0["username"]) + + uid := fmt.Sprintf("%v", r0["user_id"]) + + admintok := r0["admin_key"].(string) + readtok := r0["read_key"].(string) + sendtok := r0["send_key"].(string) + + tt.RequestAuthPatchShouldFail(t, readtok, baseUrl, "/api/users/"+uid, gin.H{"read_key": true}, 401, apierr.USER_AUTH_FAILED) + + tt.RequestAuthPatchShouldFail(t, sendtok, baseUrl, "/api/users/"+uid, gin.H{"read_key": true}, 401, apierr.USER_AUTH_FAILED) + + r1 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{}) + tt.AssertEqual(t, "admin_key", admintok, r1["admin_key"]) + tt.AssertEqual(t, "read_key", readtok, r1["read_key"]) + tt.AssertEqual(t, "send_key", sendtok, r1["send_key"]) + + r2 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{"read_key": true}) + tt.AssertEqual(t, "admin_key", admintok, r2["admin_key"]) + tt.AssertNotEqual(t, "read_key", readtok, r2["read_key"]) + tt.AssertEqual(t, "send_key", sendtok, r2["send_key"]) + readtok = r2["read_key"].(string) + + r3 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{"read_key": true, "send_key": true}) + tt.AssertEqual(t, "admin_key", admintok, r3["admin_key"]) + tt.AssertNotEqual(t, "read_key", readtok, r3["read_key"]) + tt.AssertNotEqual(t, "send_key", sendtok, r3["send_key"]) + readtok = r3["read_key"].(string) + sendtok = r3["send_key"].(string) + + r4 := tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + tt.AssertEqual(t, "admin_key", admintok, r4["admin_key"]) + tt.AssertEqual(t, "read_key", readtok, r4["read_key"]) + tt.AssertEqual(t, "send_key", sendtok, r4["send_key"]) + + r5 := tt.RequestAuthPatch[gin.H](t, admintok, baseUrl, "/api/users/"+uid, gin.H{"admin_key": true}) + tt.AssertNotEqual(t, "admin_key", admintok, r5["admin_key"]) + tt.AssertEqual(t, "read_key", readtok, r5["read_key"]) + tt.AssertEqual(t, "send_key", sendtok, r5["send_key"]) + admintokNew := r5["admin_key"].(string) + + tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/users/"+uid, 401, apierr.USER_AUTH_FAILED) + + r6 := tt.RequestAuthGet[gin.H](t, admintokNew, baseUrl, "/api/users/"+uid) + tt.AssertEqual(t, "admin_key", admintokNew, r6["admin_key"]) + tt.AssertEqual(t, "read_key", readtok, r6["read_key"]) + tt.AssertEqual(t, "send_key", sendtok, r6["send_key"]) +} + +func TestDeleteUser(t *testing.T) { + t.SkipNow() // TODO DeleteUser Not implemented + + ws, stop := tt.StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := tt.RequestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + "agent_model": "DUMMY_PHONE", + "agent_version": "4X", + "client_type": "ANDROID", + "fcm_token": "DUMMY_FCM", + }) + + uid := fmt.Sprintf("%v", r0["user_id"]) + admintok := r0["admin_key"].(string) + + tt.RequestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) + + tt.RequestAuthDelete[tt.Void](t, admintok, baseUrl, "/api/users/"+uid, nil) + + tt.RequestAuthGetShouldFail(t, admintok, baseUrl, "/api/users/"+uid, 404, apierr.USER_NOT_FOUND) - r1 := requestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid) - - assertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) - assertEqual(t, "admin_key", admintok, r1["admin_key"]) - assertEqual(t, "username", "my_user", r1["username"]) } diff --git a/server/test/util/common.go b/server/test/util/common.go new file mode 100644 index 0000000..5fdf3cb --- /dev/null +++ b/server/test/util/common.go @@ -0,0 +1,96 @@ +package util + +import ( + "fmt" + "runtime/debug" + "strings" + "testing" +) + +func AssertJsonMapEqual(t *testing.T, key string, expected map[string]any, actual map[string]any) { + mkeys := make(map[string]string) + for k := range expected { + mkeys[k] = k + } + for k := range actual { + mkeys[k] = k + } + + for mapkey := range mkeys { + + if _, ok := expected[mapkey]; !ok { + TestFailFmt(t, "Missing Key expected['%s'] ( assertJsonMapEqual[%s] )", mapkey, key) + } + if _, ok := actual[mapkey]; !ok { + TestFailFmt(t, "Missing Key actual['%s'] ( assertJsonMapEqual[%s] )", mapkey, key) + } + + AssertEqual(t, key+"."+mapkey, expected[mapkey], actual[mapkey]) + } + +} + +func AssertEqual(t *testing.T, key string, expected any, actual any) { + if expected != actual { + t.Errorf("Value [%s] differs (%T <-> %T):\n", key, expected, actual) + + str1 := fmt.Sprintf("%v", expected) + str2 := fmt.Sprintf("%v", actual) + + if strings.Contains(str1, "\n") { + t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected) + } else { + t.Errorf("Actual := \"%v\"\n", expected) + } + + if strings.Contains(str2, "\n") { + t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual) + } else { + t.Errorf("Expected := \"%v\"\n", actual) + } + + t.Error(string(debug.Stack())) + + t.FailNow() + } +} + +func AssertNotEqual(t *testing.T, key string, expected any, actual any) { + if expected == actual { + t.Errorf("Value [%s] does not differ (%T <-> %T):\n", key, expected, actual) + + str1 := fmt.Sprintf("%v", expected) + str2 := fmt.Sprintf("%v", actual) + + if strings.Contains(str1, "\n") { + t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected) + } else { + t.Errorf("Actual := \"%v\"\n", expected) + } + + if strings.Contains(str2, "\n") { + t.Errorf("Not Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual) + } else { + t.Errorf("Not Expected := \"%v\"\n", actual) + } + + t.Error(string(debug.Stack())) + + t.FailNow() + } +} + +func TestFail(t *testing.T, msg string) { + t.Error(msg) + t.FailNow() +} + +func TestFailFmt(t *testing.T, format string, args ...any) { + t.Errorf(format, args...) + t.FailNow() +} + +func TestFailErr(t *testing.T, e error) { + t.Error(fmt.Sprintf("Failed with error:\n%s\n\nError:\n%+v\n\nTrace:\n%s", e.Error(), e, string(debug.Stack()))) + t.FailNow() +} diff --git a/server/test/util/requests.go b/server/test/util/requests.go new file mode 100644 index 0000000..dc9633a --- /dev/null +++ b/server/test/util/requests.go @@ -0,0 +1,197 @@ +package util + +import ( + "blackforestbytes.com/simplecloudnotifier/api/apierr" + "bytes" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "io" + "net/http" + "testing" +) + +func RequestGet[TResult any](t *testing.T, baseURL string, urlSuffix string) TResult { + return RequestAny[TResult](t, "", "GET", baseURL, urlSuffix, nil) +} + +func RequestAuthGet[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string) TResult { + return RequestAny[TResult](t, akey, "GET", baseURL, urlSuffix, nil) +} + +func RequestPost[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, "", "POST", baseURL, urlSuffix, body) +} + +func RequestAuthPost[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, akey, "POST", baseURL, urlSuffix, body) +} + +func RequestPut[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, "", "PUT", baseURL, urlSuffix, body) +} + +func RequestAuthPUT[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, akey, "PUT", baseURL, urlSuffix, body) +} + +func RequestPatch[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, "", "PATCH", baseURL, urlSuffix, body) +} + +func RequestAuthPatch[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, akey, "PATCH", baseURL, urlSuffix, body) +} + +func RequestDelete[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, "", "DELETE", baseURL, urlSuffix, body) +} + +func RequestAuthDelete[TResult any](t *testing.T, akey string, baseURL string, urlSuffix string, body any) TResult { + return RequestAny[TResult](t, akey, "DELETE", baseURL, urlSuffix, body) +} + +func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any) TResult { + client := http.Client{} + + fmt.Printf("[-> REQUEST] (%s) %s%s [%s] [%s]\n", method, baseURL, urlSuffix, langext.Conditional(akey == "", "NO AUTH", "AUTH"), langext.Conditional(body == nil, "NO BODY", "BODY")) + + bytesbody := make([]byte, 0) + if body != nil { + bjson, err := json.Marshal(body) + if err != nil { + TestFailErr(t, err) + } + bytesbody = bjson + } + + req, err := http.NewRequest(method, baseURL+urlSuffix, bytes.NewReader(bytesbody)) + if err != nil { + TestFailErr(t, 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 { + TestFailErr(t, err) + } + defer func() { _ = resp.Body.Close() }() + + respBodyBin, err := io.ReadAll(resp.Body) + if err != nil { + TestFailErr(t, err) + } + + fmt.Println("") + fmt.Printf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode) + fmt.Println(langext.TryPrettyPrintJson(string(respBodyBin))) + fmt.Println("---------------- -------- ----------------") + fmt.Println("") + + if resp.StatusCode != 200 { + TestFail(t, "Statuscode != 200") + } + + var data TResult + if err := json.Unmarshal(respBodyBin, &data); err != nil { + TestFailErr(t, err) + } + + return data +} + +func RequestAuthGetShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "GET", baseURL, urlSuffix, nil, statusCode, errcode) +} + +func RequestAuthPostShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "POST", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestAuthPatchShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "PATCH", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestAuthDeleteShouldFail(t *testing.T, akey string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + RequestAuthAnyShouldFail(t, akey, "DELETE", baseURL, urlSuffix, body, statusCode, errcode) +} + +func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, statusCode int, errcode apierr.APIError) { + client := http.Client{} + + fmt.Printf("[-> REQUEST] (%s) %s%s [%s] (should-fail with %d/%d)\n", method, baseURL, urlSuffix, langext.Conditional(akey == "", "NO AUTH", "AUTH"), statusCode, errcode) + + bytesbody := make([]byte, 0) + if body != nil { + bjson, err := json.Marshal(body) + if err != nil { + TestFailErr(t, err) + } + bytesbody = bjson + } + + req, err := http.NewRequest(method, baseURL+urlSuffix, bytes.NewReader(bytesbody)) + if err != nil { + TestFailErr(t, 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 { + TestFailErr(t, err) + } + defer func() { _ = resp.Body.Close() }() + + respBodyBin, err := io.ReadAll(resp.Body) + if err != nil { + TestFailErr(t, err) + } + + fmt.Println("") + fmt.Printf("---------------- RESPONSE (%d) ----------------\n", resp.StatusCode) + fmt.Println(langext.TryPrettyPrintJson(string(respBodyBin))) + fmt.Println("---------------- -------- ----------------") + fmt.Println("") + + if resp.StatusCode != statusCode { + fmt.Println("Request: " + method + " :: " + baseURL + urlSuffix) + fmt.Println(string(respBodyBin)) + TestFailFmt(t, "Statuscode != %d (expected failure)", statusCode) + } + + var data gin.H + if err := json.Unmarshal(respBodyBin, &data); err != nil { + TestFailErr(t, err) + } + + if v, ok := data["success"]; ok { + if v.(bool) { + TestFail(t, "Success == true (expected failure)") + } + } else { + TestFail(t, "missing response['success']") + } + + if v, ok := data["error"]; ok { + if fmt.Sprintf("%v", v) != fmt.Sprintf("%v", errcode) { + TestFailFmt(t, "wrong errorcode (expected: %d), (actual: %v)", errcode, v) + } + } else { + TestFail(t, "missing response['error']") + } +} diff --git a/server/test/util/webserver.go b/server/test/util/webserver.go new file mode 100644 index 0000000..4f4f50f --- /dev/null +++ b/server/test/util/webserver.go @@ -0,0 +1,107 @@ +package util + +import ( + scn "blackforestbytes.com/simplecloudnotifier" + "blackforestbytes.com/simplecloudnotifier/api" + "blackforestbytes.com/simplecloudnotifier/common/ginext" + "blackforestbytes.com/simplecloudnotifier/db" + "blackforestbytes.com/simplecloudnotifier/google" + "blackforestbytes.com/simplecloudnotifier/jobs" + "blackforestbytes.com/simplecloudnotifier/logic" + "blackforestbytes.com/simplecloudnotifier/push" + "fmt" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "os" + "path/filepath" + "testing" + "time" +) + +type Void = struct{} + +func StartSimpleWebserver(t *testing.T) (*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) + + uuid2, _ := langext.NewHexUUID() + dbdir := t.TempDir() + dbfile := filepath.Join(dbdir, uuid2+".sqlite3") + + err := os.MkdirAll(dbdir, os.ModePerm) + if err != nil { + TestFailErr(t, err) + } + + f, err := os.Create(dbfile) + if err != nil { + TestFailErr(t, err) + } + err = f.Close() + if err != nil { + TestFailErr(t, err) + } + + err = os.Chmod(dbfile, 0777) + if err != nil { + TestFailErr(t, err) + } + + fmt.Println("DatabaseFile: " + dbfile) + + conf := scn.Config{ + Namespace: "test", + GinDebug: true, + ServerIP: "0.0.0.0", + ServerPort: "0", // simply choose a free port + DBFile: dbfile, + RequestTimeout: 30 * time.Second, + ReturnRawErrors: true, + DummyFirebase: true, + } + + sqlite, err := db.NewDatabase(dbfile) + if err != nil { + TestFailErr(t, err) + } + + app := logic.NewApp(sqlite) + + if err := app.Migrate(); err != nil { + TestFailErr(t, err) + } + + ginengine := ginext.NewEngine(conf) + + router := api.NewRouter(app) + + nc := push.NewTestSink() + + apc := google.NewDummy() + + jobRetry := jobs.NewDeliveryRetryJob(app) + app.Init(conf, ginengine, nc, apc, []logic.Job{jobRetry}) + + router.Init(ginengine) + + stop := func() { app.Stop(); _ = os.Remove(dbfile) } + go func() { app.Run() }() + time.Sleep(100 * time.Millisecond) + return app, stop +} diff --git a/server/test/webserver_test.go b/server/test/webserver_test.go index 61fc267..8fe3750 100644 --- a/server/test/webserver_test.go +++ b/server/test/webserver_test.go @@ -1,44 +1,45 @@ package test import ( + tt "blackforestbytes.com/simplecloudnotifier/test/util" "fmt" "testing" ) func TestWebserver(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() fmt.Printf("Port := %s\n", ws.Port) } func TestPing(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - _ = requestGet[Void](t, baseUrl, "/api/ping") - _ = requestPut[Void](t, baseUrl, "/api/ping", nil) - _ = requestPost[Void](t, baseUrl, "/api/ping", nil) - _ = requestPatch[Void](t, baseUrl, "/api/ping", nil) - _ = requestDelete[Void](t, baseUrl, "/api/ping", nil) + _ = tt.RequestGet[tt.Void](t, baseUrl, "/api/ping") + _ = tt.RequestPut[tt.Void](t, baseUrl, "/api/ping", nil) + _ = tt.RequestPost[tt.Void](t, baseUrl, "/api/ping", nil) + _ = tt.RequestPatch[tt.Void](t, baseUrl, "/api/ping", nil) + _ = tt.RequestDelete[tt.Void](t, baseUrl, "/api/ping", nil) } func TestMongo(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - _ = requestPost[Void](t, baseUrl, "/api/db-test", nil) + _ = tt.RequestPost[tt.Void](t, baseUrl, "/api/db-test", nil) } func TestHealth(t *testing.T) { - ws, stop := StartSimpleWebserver(t) + ws, stop := tt.StartSimpleWebserver(t) defer stop() baseUrl := "http://127.0.0.1:" + ws.Port - _ = requestGet[Void](t, baseUrl, "/api/health") + _ = tt.RequestGet[tt.Void](t, baseUrl, "/api/health") }