diff --git a/scnserver/test/response_test.go b/scnserver/test/response_test.go new file mode 100644 index 0000000..dc44ea5 --- /dev/null +++ b/scnserver/test/response_test.go @@ -0,0 +1,144 @@ +package test + +import ( + tt "blackforestbytes.com/simplecloudnotifier/test/util" + "fmt" + "testing" +) + +func TestResponseChannel(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels/%s", data.User[0].UID, 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", + }, + }) +} + +func TestResponseClient(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + response := tt.RequestAuthGetRaw(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients/%s", data.User[0].UID, data.User[0].Clients[0])) + + tt.AssertJsonStructureMatch(t, "json[client]", response, map[string]any{}) +} + +func TestResponseKeyToken(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + 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{}) +} + +func TestResponseMessage(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + 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])) + + tt.AssertJsonStructureMatch(t, "json[message]", response, map[string]any{}) +} + +func TestResponseSubscription(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + 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{}) +} + +func TestResponseUser(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + 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{}) +} + +func TestResponseChannelPreview(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + 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", + }, + }) +} + +func TestResponseUserPreview(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + 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{}) +} + +func TestResponseKeyTokenPreview(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + 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{}) +} diff --git a/scnserver/test/util/factory.go b/scnserver/test/util/factory.go index b7d7aff..2d829a4 100644 --- a/scnserver/test/util/factory.go +++ b/scnserver/test/util/factory.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" + "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/timeext" "gopkg.in/loremipsum.v1" "testing" @@ -59,10 +60,15 @@ type clientex struct { } type Userdat struct { - UID string - SendKey string - AdminKey string - ReadKey string + UID string + SendKey string + AdminKey string + ReadKey string + Clients []string + Channels []string + Messages []string + Keys []string + Subscriptions []string } const PX = -1 @@ -367,7 +373,8 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData { body["agent_version"] = cex.AgentVersion body["client_type"] = cex.ClientType body["fcm_token"] = cex.FCMTok - RequestAuthPost[gin.H](t, users[cex.User].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients", users[cex.User].UID), body) + r0 := RequestAuthPost[gin.H](t, users[cex.User].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/clients", users[cex.User].UID), body) + users[cex.User].Clients = append(users[cex.User].Clients, r0["client_id"].(string)) } // Create Messages @@ -398,7 +405,8 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData { body["timestamp"] = (time.Now().Add(mex.TSOffset)).Unix() } - RequestPost[gin.H](t, baseUrl, "/", body) + r0 := RequestPost[gin.H](t, baseUrl, "/", body) + users[mex.User].Messages = append(users[mex.User].Messages, r0["scn_msg_id"].(string)) } // create manual channels @@ -407,6 +415,45 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData { RequestAuthPost[Void](t, users[9].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", users[9].UID), gin.H{"name": "manual@chan"}) } + // list channels + + for i, usr := range users { + type schan struct { + ID string `json:"channel_id"` + } + type chanlist struct { + Channels []schan `json:"channels"` + } + r0 := RequestAuthGet[chanlist](t, usr.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels?selector=%s", usr.UID, "owned")) + users[i].Channels = langext.ArrMap(r0.Channels, func(v schan) string { return v.ID }) + } + + // list keys + + for i, usr := range users { + type skey struct { + ID string `json:"keytoken_id"` + } + type keylist struct { + Keys []skey `json:"channels"` + } + 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 }) + } + + // list subscriptions + + for i, usr := range users { + type ssub struct { + ID string `json:"subscription_id"` + } + type sublist struct { + Subs []ssub `json:"channels"` + } + 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 }) + } + // Sub/Unsub for Users 12+13 { @@ -474,7 +521,7 @@ func InitSingleData(t *testing.T, ws *logic.Application) SingleData { func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) { - if user == chanOwner { + if user.UID == chanOwner.UID { RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/channels", user.UID), gin.H{ "channel_owner_user_id": chanOwner.UID, diff --git a/scnserver/test/util/requests.go b/scnserver/test/util/requests.go index 6316fbf..6a94aa9 100644 --- a/scnserver/test/util/requests.go +++ b/scnserver/test/util/requests.go @@ -26,6 +26,10 @@ func RequestAuthGet[TResult any](t *testing.T, akey string, baseURL string, urlS return RequestAny[TResult](t, akey, "GET", baseURL, urlSuffix, nil, true) } +func RequestAuthGetRaw(t *testing.T, akey string, baseURL string, urlSuffix string) string { + return RequestAny[string](t, akey, "GET", baseURL, urlSuffix, nil, false) +} + func RequestPost[TResult any](t *testing.T, baseURL string, urlSuffix string, body any) TResult { return RequestAny[TResult](t, "", "POST", baseURL, urlSuffix, body, true) } @@ -166,14 +170,22 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s TestFailFmt(t, "Statuscode != 200 (actual = %d)", resp.StatusCode) } - var data TResult if deserialize { + var data TResult if err := json.Unmarshal(respBodyBin, &data); err != nil { TestFailErr(t, err) + return data + } + return data + } else { + if _, ok := (any(*new(TResult))).([]byte); ok { + return any(respBodyBin).(TResult) + } else if _, ok := (any(*new(TResult))).(string); ok { + return any(string(respBodyBin)).(TResult) + } else { + return *new(TResult) } } - - return data } func RequestAuthAnyShouldFail(t *testing.T, akey string, method string, baseURL string, urlSuffix string, body any, expectedStatusCode int, errcode apierr.APIError) { diff --git a/scnserver/test/util/structure.go b/scnserver/test/util/structure.go new file mode 100644 index 0000000..260a791 --- /dev/null +++ b/scnserver/test/util/structure.go @@ -0,0 +1,118 @@ +package util + +import ( + "encoding/json" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "reflect" + "testing" + "time" +) + +func AssertJsonStructureMatch(t *testing.T, key string, jsonData string, expected map[string]any) { + + realData := make(map[string]any) + + err := json.Unmarshal([]byte(jsonData), &realData) + if err != nil { + t.Errorf("Failed to decode json of [%s]: %s", key, err.Error()) + return + } + + AssertJsonStructureMatchOfMap(t, key, realData, expected) +} + +func AssertJsonStructureMatchOfMap(t *testing.T, key string, realData map[string]any, expected map[string]any) { + + for k := range expected { + if _, ok := realData[k]; !ok { + t.Errorf("Missing Key in data '%s': [[%s]]", key, k) + } + } + + for k := range realData { + if _, ok := expected[k]; !ok { + t.Errorf("Additional key in data '%s': [[%s]]", key, k) + } + } + + for k, v := range realData { + + schema, ok := expected[k] + + if !ok { + 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) + } + } +}