diff --git a/scnserver/TODO.md b/scnserver/TODO.md index 4c1d3f6..f947712 100644 --- a/scnserver/TODO.md +++ b/scnserver/TODO.md @@ -11,6 +11,8 @@ - ios purchase verification - (!) use goext.ginWrapper + + - (!!!) local lock to prevent database-locked errors (there are a lot when one client malfunctions and starts sending a lot of notifications) #### UNSURE @@ -66,4 +68,4 @@ #### FUTURE - - Remove compat, especially do not create compat id for every new message... \ No newline at end of file + - Remove compat, especially do not create compat id for every new message... diff --git a/scnserver/test/response_test.go b/scnserver/test/response_test.go index 3bb290f..48ba8d4 100644 --- a/scnserver/test/response_test.go +++ b/scnserver/test/response_test.go @@ -3,6 +3,7 @@ package test import ( tt "blackforestbytes.com/simplecloudnotifier/test/util" "fmt" + "github.com/gin-gonic/gin" "testing" ) @@ -42,12 +43,21 @@ func TestResponseClient(t *testing.T) { data := tt.InitDefaultData(t, ws) - response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients/%s", data.User[2].UID, data.User[2].Clients[2])) + response := tt.RequestAuthGetRaw(t, data.User[2].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients/%s", data.User[2].UID, data.User[2].Clients[0])) - tt.AssertJsonStructureMatch(t, "json[client]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[client]", response, map[string]any{ + "client_id": "id", + "user_id": "id", + "type": "string", + "fcm_token": "string", + "timestamp_created": "rfc3339", + "agent_model": "string", + "agent_version": "string", + "name": "string|null", + }) } -func TestResponseKeyToken(t *testing.T) { +func TestResponseKeyToken1(t *testing.T) { ws, baseUrl, stop := tt.StartSimpleWebserver(t) defer stop() @@ -55,7 +65,52 @@ func TestResponseKeyToken(t *testing.T) { response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.User[0].UID, data.User[0].Keys[0])) - tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{ + "keytoken_id": "id", + "name": "string", + "timestamp_created": "rfc3339", + "timestamp_lastused": "rfc3339|null", + "owner_user_id": "id", + "all_channels": "bool", + "channels": []any{"string"}, + "permissions": "string", + "messages_sent": "int", + }) +} + +func TestResponseKeyToken2(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitSingleData(t, ws) + + chan1 := tt.RequestAuthPost[gin.H](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", data.UID), gin.H{ + "name": "TestChan1asdf", + }) + + type keyobj struct { + KeytokenId string `json:"keytoken_id"` + } + k0 := tt.RequestAuthPost[keyobj](t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.UID), gin.H{ + "all_channels": false, + "channels": []string{chan1["channel_id"].(string)}, + "name": "TKey1", + "permissions": "CS", + }) + + response := tt.RequestAuthGetRaw(t, data.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys/%s", data.UID, k0.KeytokenId)) + + tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{ + "keytoken_id": "id", + "name": "string", + "timestamp_created": "rfc3339", + "timestamp_lastused": "rfc3339|null", + "owner_user_id": "id", + "all_channels": "bool", + "channels": []any{"string"}, + "permissions": "string", + "messages_sent": "int", + }) } func TestResponseMessage(t *testing.T) { @@ -64,9 +119,23 @@ func TestResponseMessage(t *testing.T) { data := tt.InitDefaultData(t, ws) - response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/messages/%s", data.User[0].UID, data.User[0].Messages[0])) + response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/messages/%s", data.User[0].Messages[0])) - tt.AssertJsonStructureMatch(t, "json[message]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[message]", response, map[string]any{ + "message_id": "id", + "sender_user_id": "id", + "channel_internal_name": "string", + "channel_id": "id", + "sender_name": "string", + "sender_ip": "string", + "timestamp": "rfc3339", + "title": "string", + "content": "null", + "priority": "int", + "usr_message_id": "null", + "used_key_id": "id", + "trimmed": "bool", + }) } func TestResponseSubscription(t *testing.T) { @@ -77,7 +146,15 @@ func TestResponseSubscription(t *testing.T) { response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions/%s", data.User[0].UID, data.User[0].Subscriptions[0])) - tt.AssertJsonStructureMatch(t, "json[subscription]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[subscription]", response, map[string]any{ + "subscription_id": "id", + "subscriber_user_id": "id", + "channel_owner_user_id": "id", + "channel_id": "id", + "channel_internal_name": "string", + "timestamp_created": "rfc3339", + "confirmed": "bool", + }) } func TestResponseUser(t *testing.T) { @@ -88,7 +165,26 @@ func TestResponseUser(t *testing.T) { response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s", data.User[0].UID)) - tt.AssertJsonStructureMatch(t, "json[user]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[user]", response, map[string]any{ + "user_id": "id", + "username": "null", + "timestamp_created": "rfc3339", + "timestamp_lastread": "null", + "timestamp_lastsent": "rfc3339", + "messages_sent": "int", + "quota_used": "int", + "quota_remaining": "int", + "quota_max": "int", + "is_pro": "bool", + "default_channel": "string", + "max_body_size": "int", + "max_title_length": "int", + "default_priority": "int", + "max_channel_name_length": "int", + "max_channel_description_length": "int", + "max_sender_name_length": "int", + "max_user_message_id_length": "int", + }) } func TestResponseChannelPreview(t *testing.T) { @@ -100,24 +196,11 @@ func TestResponseChannelPreview(t *testing.T) { response := tt.RequestAuthGetRaw(t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/channels/%s", data.User[0].Channels[0])) tt.AssertJsonStructureMatch(t, "json[channel]", response, map[string]any{ - "channel_id": "id", - "owner_user_id": "id", - "internal_name": "string", - "display_name": "string", - "description_name": "null", - "subscribe_key": "string", - "timestamp_created": "rfc3339", - "timestamp_lastsent": "rfc3339", - "messages_sent": "int", - "subscription": map[string]any{ - "subscription_id": "id", - "subscriber_user_id": "id", - "channel_owner_user_id": "id", - "channel_id": "id", - "channel_internal_name": "string", - "timestamp_created": "rfc3339", - "confirmed": "bool", - }, + "channel_id": "id", + "owner_user_id": "id", + "internal_name": "string", + "display_name": "string", + "description_name": "string|null", }) } @@ -129,7 +212,10 @@ func TestResponseUserPreview(t *testing.T) { response := tt.RequestAuthGetRaw(t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/users/%s", data.User[0].UID)) - tt.AssertJsonStructureMatch(t, "json[user]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[user]", response, map[string]any{ + "user_id": "id", + "username": "string|null", + }) } func TestResponseKeyTokenPreview(t *testing.T) { @@ -140,5 +226,12 @@ func TestResponseKeyTokenPreview(t *testing.T) { response := tt.RequestAuthGetRaw(t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].Keys[0])) - tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{}) + tt.AssertJsonStructureMatch(t, "json[key]", response, map[string]any{ + "keytoken_id": "id", + "name": "string", + "owner_user_id": "id", + "all_channels": "bool", + "channels": []any{"id"}, + "permissions": "string", + }) } diff --git a/scnserver/test/util/factory.go b/scnserver/test/util/factory.go index 2d829a4..f642369 100644 --- a/scnserver/test/util/factory.go +++ b/scnserver/test/util/factory.go @@ -435,7 +435,7 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData { ID string `json:"keytoken_id"` } type keylist struct { - Keys []skey `json:"channels"` + Keys []skey `json:"keys"` } r0 := RequestAuthGet[keylist](t, usr.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", usr.UID)) users[i].Keys = langext.ArrMap(r0.Keys, func(v skey) string { return v.ID }) @@ -448,10 +448,10 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData { ID string `json:"subscription_id"` } type sublist struct { - Subs []ssub `json:"channels"` + Subs []ssub `json:"subscriptions"` } r0 := RequestAuthGet[sublist](t, usr.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/subscriptions?direction=%s&confirmation=%s", usr.UID, "outgoing", "confirmed")) - users[i].Keys = langext.ArrMap(r0.Subs, func(v ssub) string { return v.ID }) + users[i].Subscriptions = langext.ArrMap(r0.Subs, func(v ssub) string { return v.ID }) } // Sub/Unsub for Users 12+13 @@ -510,13 +510,15 @@ func InitSingleData(t *testing.T, ws *logic.Application) SingleData { success = true - return SingleData{ + sd := SingleData{ UID: r0.UserId, AdminKey: r0.AdminKey, SendKey: r0.SendKey, ReadKey: r0.ReadKey, ClientID: r0.Clients[0].ClientId, } + + return sd } func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) { diff --git a/scnserver/test/util/structure.go b/scnserver/test/util/structure.go index 260a791..1ce95eb 100644 --- a/scnserver/test/util/structure.go +++ b/scnserver/test/util/structure.go @@ -2,6 +2,7 @@ package util import ( "encoding/json" + "fmt" "gogs.mikescher.com/BlackForestBytes/goext/langext" "reflect" "testing" @@ -18,101 +19,158 @@ func AssertJsonStructureMatch(t *testing.T, key string, jsonData string, expecte return } - AssertJsonStructureMatchOfMap(t, key, realData, expected) + assertjsonStructureMatchMapObject(t, expected, realData, key) } -func AssertJsonStructureMatchOfMap(t *testing.T, key string, realData map[string]any, expected map[string]any) { +func assertJsonStructureMatch(t *testing.T, schema any, realValue any, keyPath string) { - for k := range expected { - if _, ok := realData[k]; !ok { - t.Errorf("Missing Key in data '%s': [[%s]]", key, k) + if strschema, ok := schema.(string); ok { + + assertjsonStructureMatchSingleValue(t, strschema, realValue, keyPath) + + } else if mapschema, ok := schema.(map[string]any); ok { + + if reflect.ValueOf(realValue).Kind() != reflect.Map { + t.Errorf("Key < %s > is not a object (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + if _, ok := realValue.(map[string]any); !ok { + t.Errorf("Key < %s > is not a object[recursive] (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + + assertjsonStructureMatchMapObject(t, mapschema, realValue.(map[string]any), keyPath) + + } else if arrschema, ok := schema.([]any); ok && len(arrschema) == 1 { + + if _, ok := realValue.([]any); !ok { + t.Errorf("Key < %s > is not a array[recursive] (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + + assertjsonStructureMatchArray(t, arrschema, realValue.([]any), keyPath) + + } else { + t.Errorf("Unknown schema type '%s' for key < %s >", schema, keyPath) + } +} + +func assertjsonStructureMatchSingleValue(t *testing.T, strschema string, realValue any, keyPath string) { + switch strschema { + case "id": + if _, ok := realValue.(string); !ok { + t.Errorf("Key < %s > is not a string (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + if len(realValue.(string)) != 24 { //TODO validate checksum? + t.Errorf("Key < %s > is not a valid entity-id date (its '%v')", keyPath, realValue) + return + } + case "string": + if _, ok := realValue.(string); !ok { + t.Errorf("Key < %s > is not a string (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + case "null": + if !langext.IsNil(realValue) { + t.Errorf("Key < %s > is not a NULL (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + case "string|null": + if langext.IsNil(realValue) { + return // OK + } else if _, ok := realValue.(string); !ok { + return // OK + } else { + t.Errorf("Key < %s > is not a string|null (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + case "rfc3339": + if _, ok := realValue.(string); !ok { + t.Errorf("Key < %s > is not a string (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + if _, err := time.Parse(time.RFC3339, realValue.(string)); err != nil { + t.Errorf("Key < %s > is not a valid rfc3339 date (its '%v')", keyPath, realValue) + return + } + case "rfc3339|null": + if langext.IsNil(realValue) { + return // OK + } + if _, ok := realValue.(string); !ok { + t.Errorf("Key < %s > is not a string (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + if _, err := time.Parse(time.RFC3339, realValue.(string)); err != nil { + t.Errorf("Key < %s > is not a valid rfc3339 date (its '%v')", keyPath, realValue) + return + } + case "int": + if _, ok := realValue.(float64); !ok { + t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + if realValue.(float64) != float64(int(realValue.(float64))) { + t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + case "float": + if _, ok := realValue.(float64); !ok { + t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + case "bool": + if _, ok := realValue.(bool); !ok { + t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + default: + t.Errorf("Unknown schema type '%s' for key < %s >", strschema, keyPath) + return + } +} + +func assertjsonStructureMatchMapObject(t *testing.T, mapschema map[string]any, realValue map[string]any, keyPath string) { + + for k := range mapschema { + if _, ok := realValue[k]; !ok { + t.Errorf("Missing Key: < %s >", keyPath) } } - for k := range realData { - if _, ok := expected[k]; !ok { - t.Errorf("Additional key in data '%s': [[%s]]", key, k) + for k := range realValue { + if _, ok := mapschema[k]; !ok { + t.Errorf("Additional key: < %s >", keyPath) } } - for k, v := range realData { + for k, v := range realValue { - schema, ok := expected[k] + kpath := keyPath + "." + k + + schema, ok := mapschema[k] if !ok { + t.Errorf("Key < %s > is missing in response", kpath) continue } - if strschema, ok := schema.(string); ok { - switch strschema { - case "id": - if _, ok := v.(string); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a string (its actually %T: '%v')", k, key, v, v) - continue - } - if len(v.(string)) != 24 { //TODO validate checksum? - t.Errorf("Key [[%s]] in data '%s' is not a valid entity-id date (its '%v')", k, key, v) - continue - } - case "string": - if _, ok := v.(string); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a string (its actually %T: '%v')", k, key, v, v) - continue - } - case "null": - if !langext.IsNil(v) { - t.Errorf("Key [[%s]] in data '%s' is not a NULL (its actually %T: '%v')", k, key, v, v) - continue - } - case "rfc3339": - if _, ok := v.(string); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a string (its actually %T: '%v')", k, key, v, v) - continue - } - if _, err := time.Parse(time.RFC3339, v.(string)); err != nil { - t.Errorf("Key [[%s]] in data '%s' is not a valid rfc3339 date (its '%v')", k, key, v) - continue - } - case "int": - if _, ok := v.(float64); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a int (its actually %T: '%v')", k, key, v, v) - continue - } - if v.(float64) != float64(int(v.(float64))) { - t.Errorf("Key [[%s]] in data '%s' is not a int (its actually %T: '%v')", k, key, v, v) - continue - } - case "float": - if _, ok := v.(float64); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a int (its actually %T: '%v')", k, key, v, v) - continue - } - case "bool": - if _, ok := v.(bool); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a int (its actually %T: '%v')", k, key, v, v) - continue - } - case "object": - if reflect.ValueOf(v).Kind() != reflect.Map { - t.Errorf("Key [[%s]] in data '%s' is not a object (its actually %T: '%v')", k, key, v, v) - continue - } - case "array": - if reflect.ValueOf(v).Kind() != reflect.Array { - t.Errorf("Key [[%s]] in data '%s' is not a array (its actually %T: '%v')", k, key, v, v) - continue - } - } - } else if mapschema, ok := schema.(map[string]any); ok { - if reflect.ValueOf(v).Kind() != reflect.Map { - t.Errorf("Key [[%s]] in data '%s' is not a object (its actually %T: '%v')", k, key, v, v) - continue - } - if _, ok := v.(map[string]any); !ok { - t.Errorf("Key [[%s]] in data '%s' is not a object[recursive] (its actually %T: '%v')", k, key, v, v) - continue - } - AssertJsonStructureMatchOfMap(t, key+".["+k+"]", v.(map[string]any), mapschema) - } + assertJsonStructureMatch(t, schema, v, kpath) + } + +} + +func assertjsonStructureMatchArray(t *testing.T, arrschema []any, realValue []any, keyPath string) { + + if len(arrschema) != 1 { + t.Errorf("Array schema must have exactly one element, but got %d", len(arrschema)) + return + } + + for i, realArrVal := range realValue { + assertJsonStructureMatch(t, arrschema[0], realArrVal, fmt.Sprintf("%s[%d]", keyPath, i)) + } + }