diff --git a/server/api/handler/api.go b/server/api/handler/api.go index c41178f..70ccdb7 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -118,6 +118,11 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse { if b.NoClient { return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSONWithClients(make([]models.Client, 0)))) } else { + err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) + } + 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) @@ -330,7 +335,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse { // GetClient swaggerdoc // -// @Summary Get a single clients +// @Summary Get a single client // @ID api-clients-get // @Tags API-v2 // @@ -421,6 +426,11 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { return *permResp } + err := h.database.DeleteClientsByFCM(ctx, b.FCMToken) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete existing clients in db", err) + } + 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 client in db", err) @@ -444,7 +454,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse { // @Failure 404 {object} ginresp.apiError // @Failure 500 {object} ginresp.apiError // -// @Router /api/users/{uid}/clients [DELETE] +// @Router /api/users/{uid}/clients/{cid} [DELETE] func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse { type uri struct { UserID models.UserID `uri:"uid"` diff --git a/server/api/router.go b/server/api/router.go index 2453af9..d83c428 100644 --- a/server/api/router.go +++ b/server/api/router.go @@ -118,7 +118,7 @@ func (r *Router) Init(e *gin.Engine) { apiv2.GET("/users/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients)) apiv2.GET("/users/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient)) apiv2.POST("/users/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient)) - apiv2.DELETE("/users/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient)) + apiv2.DELETE("/users/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.DeleteClient)) apiv2.GET("/users/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels)) apiv2.GET("/users/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel)) diff --git a/server/db/clients.go b/server/db/clients.go index c3cd22d..df3c01c 100644 --- a/server/db/clients.go +++ b/server/db/clients.go @@ -106,3 +106,17 @@ func (db *Database) DeleteClient(ctx TxContext, clientid models.ClientID) error return nil } + +func (db *Database) DeleteClientsByFCM(ctx TxContext, fcmtoken string) error { + tx, err := ctx.GetOrCreateTransaction(db) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE fcm_token = ?", fcmtoken) + if err != nil { + return err + } + + return nil +} diff --git a/server/test/clients_test.go b/server/test/clients_test.go new file mode 100644 index 0000000..ff4f080 --- /dev/null +++ b/server/test/clients_test.go @@ -0,0 +1,152 @@ +package test + +import ( + "fmt" + "github.com/gin-gonic/gin" + "testing" +) + +func TestGetClient(t *testing.T) { + ws, stop := StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := 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"]) + + 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) + + assertEqual(t, "uid", uid, fmt.Sprintf("%v", r1["user_id"])) + assertEqual(t, "admin_key", admintok, r1["admin_key"]) + assertEqual(t, "username", nil, r1["username"]) + + type rt2 struct { + Clients []gin.H `json:"clients"` + } + + r2 := requestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + + 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"])) + + cid := fmt.Sprintf("%v", c0["client_id"]) + + r3 := requestAuthGet[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients/"+cid) + + assertJsonMapEqual(t, "client", r3, c0) +} + +func TestCreateAndDeleteClient(t *testing.T) { + ws, stop := StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := 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"]) + + 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{ + "agent_model": "DUMMY_PHONE_2", + "agent_version": "99X", + "client_type": "IOS", + "fcm_token": "DUMMY_FCM_2", + }) + + cid2 := fmt.Sprintf("%v", r2["client_id"]) + + type rt3 struct { + Clients []gin.H `json:"clients"` + } + + r3 := requestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + 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"])) + + r5 := requestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + assertEqual(t, "len(clients)", 1, len(r5.Clients)) +} + +func TestReuseFCM(t *testing.T) { + ws, stop := StartSimpleWebserver(t) + defer stop() + + baseUrl := "http://127.0.0.1:" + ws.Port + + r0 := requestPost[gin.H](t, baseUrl, "/api/users", gin.H{ + "agent_model": "DUMMY_PHONE", + "agent_version": "4X", + "client_type": "ANDROID", + "fcm_token": "DUMMY_FCM_001", + }) + + uid := fmt.Sprintf("%v", r0["user_id"]) + + 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) + + type rt2 struct { + Clients []gin.H `json:"clients"` + } + + r1 := requestAuthGet[rt2](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + + assertEqual(t, "len(clients)", 1, len(r1.Clients)) + + r2 := requestAuthPost[gin.H](t, admintok, baseUrl, "/api/users/"+uid+"/clients", gin.H{ + "agent_model": "DUMMY_PHONE_2", + "agent_version": "99X", + "client_type": "IOS", + "fcm_token": "DUMMY_FCM_001", + }) + + cid2 := fmt.Sprintf("%v", r2["client_id"]) + + type rt3 struct { + Clients []gin.H `json:"clients"` + } + + r3 := requestAuthGet[rt3](t, admintok, baseUrl, "/api/users/"+uid+"/clients") + assertEqual(t, "len(clients)", 1, len(r3.Clients)) + + 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 index f7d92eb..8c02976 100644 --- a/server/test/common_test.go +++ b/server/test/common_test.go @@ -202,6 +202,29 @@ func requestAny[TResult any](t *testing.T, akey string, method string, baseURL s 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) @@ -210,17 +233,19 @@ func assertEqual(t *testing.T, key string, expected any, actual any) { str2 := fmt.Sprintf("%v", actual) if strings.Contains(str1, "\n") { - t.Errorf("Actual :\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected) + t.Errorf("Actual:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", expected) } else { - t.Errorf("Actual : \"%v\"\n", expected) + t.Errorf("Actual := \"%v\"\n", expected) } if strings.Contains(str2, "\n") { - t.Errorf("Expected :\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual) + t.Errorf("Expected:\n~~~~~~~~~~~~~~~~\n%v\n~~~~~~~~~~~~~~~~\n\n", actual) } else { - t.Errorf("Expected : \"%v\"\n", actual) + t.Errorf("Expected := \"%v\"\n", actual) } + t.Error(debug.Stack()) + t.FailNow() } }