From c0b8a8a3f4db543b7989510a4d69c6ebd822be09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sat, 12 Apr 2025 23:37:06 +0200 Subject: [PATCH] Return subscription from channel-preview [skip-tests] --- scnserver/api/handler/apiPreview.go | 11 +- scnserver/models/channel.go | 5 +- scnserver/test/preview_test.go | 178 +++++++++++++++++++++++++++- scnserver/test/response_test.go | 1 + scnserver/test/util/common.go | 16 +++ scnserver/test/util/structure.go | 12 ++ 6 files changed, 217 insertions(+), 6 deletions(-) diff --git a/scnserver/api/handler/apiPreview.go b/scnserver/api/handler/apiPreview.go index 2d027f4..b3d2264 100644 --- a/scnserver/api/handler/apiPreview.go +++ b/scnserver/api/handler/apiPreview.go @@ -90,6 +90,8 @@ func (h APIHandler) GetChannelPreview(pctx ginext.PreContext) ginext.HTTPRespons return *permResp } + userid := *ctx.GetPermissionUserID() + channel, err := h.database.GetChannelByID(ctx, u.ChannelID) if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.CHANNEL_NOT_FOUND, "Channel not found", err) @@ -98,7 +100,12 @@ func (h APIHandler) GetChannelPreview(pctx ginext.PreContext) ginext.HTTPRespons return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err) } - return finishSuccess(ginext.JSON(http.StatusOK, channel.Preview())) + sub, err := h.database.GetSubscriptionBySubscriber(ctx, userid, channel.ChannelID) + if err != nil { + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) + } + + return finishSuccess(ginext.JSON(http.StatusOK, channel.Preview(sub))) }) } @@ -155,7 +162,7 @@ func (h APIHandler) GetUserKeyPreview(pctx ginext.PreContext) ginext.HTTPRespons // Query by token.token keytoken, err := h.database.GetKeyTokenByToken(ctx, u.KeyID) - if errors.Is(err, sql.ErrNoRows) { + if keytoken == nil { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { diff --git a/scnserver/models/channel.go b/scnserver/models/channel.go index 4e59529..416a725 100644 --- a/scnserver/models/channel.go +++ b/scnserver/models/channel.go @@ -24,6 +24,8 @@ type ChannelPreview struct { DisplayName string `json:"display_name"` DescriptionName *string `json:"description_name"` MessagesSent int `json:"messages_sent"` + + Subscription *Subscription `json:"subscription"` } func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription { @@ -33,7 +35,7 @@ func (c Channel) WithSubscription(sub *Subscription) ChannelWithSubscription { } } -func (c Channel) Preview() ChannelPreview { +func (c Channel) Preview(sub *Subscription) ChannelPreview { return ChannelPreview{ ChannelID: c.ChannelID, OwnerUserID: c.OwnerUserID, @@ -41,5 +43,6 @@ func (c Channel) Preview() ChannelPreview { DisplayName: c.DisplayName, DescriptionName: c.DescriptionName, MessagesSent: c.MessagesSent, + Subscription: sub, } } diff --git a/scnserver/test/preview_test.go b/scnserver/test/preview_test.go index 0de37d1..43f109b 100644 --- a/scnserver/test/preview_test.go +++ b/scnserver/test/preview_test.go @@ -1,12 +1,13 @@ package test import ( + "fmt" + "testing" + "blackforestbytes.com/simplecloudnotifier/api/apierr" tt "blackforestbytes.com/simplecloudnotifier/test/util" - "fmt" "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" - "testing" ) func TestGetChannelPreview(t *testing.T) { @@ -148,7 +149,7 @@ func TestGetUserPreview(t *testing.T) { } -func TestGetKeyTokenPreview(t *testing.T) { +func TestGetKeyTokenPreviewByID(t *testing.T) { ws, baseUrl, stop := tt.StartSimpleWebserver(t) defer stop() @@ -248,3 +249,174 @@ func TestGetKeyTokenPreview(t *testing.T) { } } + +func TestGetKeyTokenPreviewByToken(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + type keyobj struct { + AllChannels bool `json:"all_channels"` + Channels []string `json:"channels"` + KeytokenId string `json:"keytoken_id"` + MessagesSent int `json:"messages_sent"` + Name string `json:"name"` + OwnerUserId string `json:"owner_user_id"` + Permissions string `json:"permissions"` + } + type keylist struct { + Keys []keyobj `json:"keys"` + } + + klist := tt.RequestAuthGet[keylist](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/users/%s/keys", data.User[0].UID)) + + adminKeyObj := *langext.ArrFirstOrNil(klist.Keys, func(v keyobj) bool { return v.Permissions == "A" }) + readKeyObj := *langext.ArrFirstOrNil(klist.Keys, func(v keyobj) bool { return v.Permissions == "UR;CR" }) + sendKeyObj := *langext.ArrFirstOrNil(klist.Keys, func(v keyobj) bool { return v.Permissions == "CS" }) + + // Test with User[0]'s keys accessing their own key previews via token value + { + rqAdmin := tt.RequestAuthGet[gin.H](t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].AdminKey)) + tt.AssertEqual(t, "keytoken_id", adminKeyObj.KeytokenId, rqAdmin["keytoken_id"]) + tt.AssertEqual(t, "name", adminKeyObj.Name, rqAdmin["name"]) + tt.AssertEqual(t, "owner_user_id", adminKeyObj.OwnerUserId, rqAdmin["owner_user_id"]) + tt.AssertEqual(t, "permissions", adminKeyObj.Permissions, rqAdmin["permissions"]) + + rqRead := tt.RequestAuthGet[gin.H](t, data.User[0].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].ReadKey)) + tt.AssertEqual(t, "keytoken_id", readKeyObj.KeytokenId, rqRead["keytoken_id"]) + tt.AssertEqual(t, "name", readKeyObj.Name, rqRead["name"]) + tt.AssertEqual(t, "owner_user_id", readKeyObj.OwnerUserId, rqRead["owner_user_id"]) + tt.AssertEqual(t, "permissions", readKeyObj.Permissions, rqRead["permissions"]) + + rqSend := tt.RequestAuthGet[gin.H](t, data.User[0].SendKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].SendKey)) + tt.AssertEqual(t, "keytoken_id", sendKeyObj.KeytokenId, rqSend["keytoken_id"]) + tt.AssertEqual(t, "name", sendKeyObj.Name, rqSend["name"]) + tt.AssertEqual(t, "owner_user_id", sendKeyObj.OwnerUserId, rqSend["owner_user_id"]) + tt.AssertEqual(t, "permissions", sendKeyObj.Permissions, rqSend["permissions"]) + } + + // Test with User[1]'s keys accessing User[0]'s key previews via token value (should work as preview is public) + { + rqAdmin := tt.RequestAuthGet[gin.H](t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].AdminKey)) + tt.AssertEqual(t, "keytoken_id", adminKeyObj.KeytokenId, rqAdmin["keytoken_id"]) + tt.AssertEqual(t, "name", adminKeyObj.Name, rqAdmin["name"]) + tt.AssertEqual(t, "owner_user_id", adminKeyObj.OwnerUserId, rqAdmin["owner_user_id"]) + tt.AssertEqual(t, "permissions", adminKeyObj.Permissions, rqAdmin["permissions"]) + + rqRead := tt.RequestAuthGet[gin.H](t, data.User[1].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].ReadKey)) + tt.AssertEqual(t, "keytoken_id", readKeyObj.KeytokenId, rqRead["keytoken_id"]) + tt.AssertEqual(t, "name", readKeyObj.Name, rqRead["name"]) + tt.AssertEqual(t, "owner_user_id", readKeyObj.OwnerUserId, rqRead["owner_user_id"]) + tt.AssertEqual(t, "permissions", readKeyObj.Permissions, rqRead["permissions"]) + + rqSend := tt.RequestAuthGet[gin.H](t, data.User[1].SendKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].SendKey)) + tt.AssertEqual(t, "keytoken_id", sendKeyObj.KeytokenId, rqSend["keytoken_id"]) + tt.AssertEqual(t, "name", sendKeyObj.Name, rqSend["name"]) + tt.AssertEqual(t, "owner_user_id", sendKeyObj.OwnerUserId, rqSend["owner_user_id"]) + tt.AssertEqual(t, "permissions", sendKeyObj.Permissions, rqSend["permissions"]) + } + + // Test with User[1]'s keys accessing User[0]'s key previews via token value (should work as preview is public) + { + rqAdmin := tt.RequestAuthGet[gin.H](t, data.User[1].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].AdminKey)) + tt.AssertEqual(t, "keytoken_id", adminKeyObj.KeytokenId, rqAdmin["keytoken_id"]) + tt.AssertEqual(t, "name", adminKeyObj.Name, rqAdmin["name"]) + tt.AssertEqual(t, "owner_user_id", adminKeyObj.OwnerUserId, rqAdmin["owner_user_id"]) + tt.AssertEqual(t, "permissions", adminKeyObj.Permissions, rqAdmin["permissions"]) + + rqRead := tt.RequestAuthGet[gin.H](t, data.User[1].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].ReadKey)) + tt.AssertEqual(t, "keytoken_id", readKeyObj.KeytokenId, rqRead["keytoken_id"]) + tt.AssertEqual(t, "name", readKeyObj.Name, rqRead["name"]) + tt.AssertEqual(t, "owner_user_id", readKeyObj.OwnerUserId, rqRead["owner_user_id"]) + tt.AssertEqual(t, "permissions", readKeyObj.Permissions, rqRead["permissions"]) + + rqSend := tt.RequestAuthGet[gin.H](t, data.User[1].SendKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].SendKey)) + tt.AssertEqual(t, "keytoken_id", sendKeyObj.KeytokenId, rqSend["keytoken_id"]) + tt.AssertEqual(t, "name", sendKeyObj.Name, rqSend["name"]) + tt.AssertEqual(t, "owner_user_id", sendKeyObj.OwnerUserId, rqSend["owner_user_id"]) + tt.AssertEqual(t, "permissions", sendKeyObj.Permissions, rqSend["permissions"]) + } + + // Test with invalid token + { + tt.RequestAuthGetShouldFail(t, data.User[0].AdminKey, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", "invalid-token-string"), 404, apierr.KEY_NOT_FOUND) + } + + // Test without auth + { + tt.RequestGetShouldFail(t, baseUrl, fmt.Sprintf("/api/v2/preview/keys/%s", data.User[0].SendKey), 401, apierr.USER_AUTH_FAILED) + } + +} + +func TestGetChannelPreviewSubscriptionNone(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + // User[1] (who is not subscribed) requests preview of User[0]'s channel + preview := tt.RequestAuthGet[gin.H](t, data.User[1].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/channels/%s", data.User[0].Channels[0].ChannelID)) + + // Assert User[1] sees no subscription + tt.AssertNil(t, "subscription", preview["subscription"]) +} + +func TestGetChannelPreviewSubscriptionUnconfirmed(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + chanReq := *langext.ArrFirstOrNil(data.User[15].Channels, func(v tt.ChanData) bool { return v.InternalName == "chan_other_request" }) + + preview := tt.RequestAuthGet[gin.H](t, data.User[14].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/channels/%s", chanReq.ChannelID)) + + // Assert User[14] sees their unconfirmed subscription + tt.AssertNotNil(t, "subscription", preview["subscription"]) + subscription, ok := preview["subscription"].(map[string]any) + tt.AssertTrue(t, "subscription is map", ok) + tt.AssertEqual(t, "subscription.user_id", data.User[14].UID, subscription["subscriber_user_id"]) + tt.AssertEqual(t, "subscription.user_id", data.User[15].UID, subscription["channel_owner_user_id"]) + tt.AssertEqual(t, "subscription.channel_id", chanReq.ChannelID, subscription["channel_id"]) + tt.AssertEqual(t, "subscription.confirmed", false, subscription["confirmed"]) +} + +func TestGetChannelPreviewSubscriptionOwnSubscription(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + preview := tt.RequestAuthGet[gin.H](t, data.User[4].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/channels/%s", data.User[4].Channels[0].ChannelID)) + + // Assert User[4] sees their own subscription + tt.AssertNotNil(t, "subscription", preview["subscription"]) + subscription, ok := preview["subscription"].(map[string]any) + tt.AssertTrue(t, "subscription is map", ok) + tt.AssertEqual(t, "subscription.user_id", data.User[4].UID, subscription["subscriber_user_id"]) + tt.AssertEqual(t, "subscription.user_id", data.User[4].UID, subscription["channel_owner_user_id"]) + tt.AssertEqual(t, "subscription.channel_id", data.User[4].Channels[0].ChannelID, subscription["channel_id"]) + tt.AssertEqual(t, "subscription.confirmed", true, subscription["confirmed"]) +} + +func TestGetChannelPreviewSubscriptionForeignSubscription(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + chanReq := *langext.ArrFirstOrNil(data.User[15].Channels, func(v tt.ChanData) bool { return v.InternalName == "chan_other_accepted" }) + + preview := tt.RequestAuthGet[gin.H](t, data.User[14].ReadKey, baseUrl, fmt.Sprintf("/api/v2/preview/channels/%s", chanReq.ChannelID)) + + // Assert User[14] sees their confirmed subscription + tt.AssertNotNil(t, "subscription", preview["subscription"]) + subscription, ok := preview["subscription"].(map[string]any) + tt.AssertTrue(t, "subscription is map", ok) + tt.AssertEqual(t, "subscription.user_id", data.User[14].UID, subscription["subscriber_user_id"]) + tt.AssertEqual(t, "subscription.user_id", data.User[15].UID, subscription["channel_owner_user_id"]) + tt.AssertEqual(t, "subscription.channel_id", chanReq.ChannelID, subscription["channel_id"]) + tt.AssertEqual(t, "subscription.confirmed", true, subscription["confirmed"]) +} diff --git a/scnserver/test/response_test.go b/scnserver/test/response_test.go index a0a144a..94fd43e 100644 --- a/scnserver/test/response_test.go +++ b/scnserver/test/response_test.go @@ -255,6 +255,7 @@ func TestResponseChannelPreview(t *testing.T) { "display_name": "string", "description_name": "string|null", "messages_sent": "int", + "subscription": "object|null", }) } diff --git a/scnserver/test/util/common.go b/scnserver/test/util/common.go index 6b86f7e..d59d0af 100644 --- a/scnserver/test/util/common.go +++ b/scnserver/test/util/common.go @@ -306,6 +306,22 @@ func AssertAny(v any) { // used to prevent golang "unused variable error" } +func AssertNil(t *testing.T, key string, v any) { + if v != nil { + t.Errorf("AssertNil(%s) failed - actual value:\n%+v", key, v) + t.Error(string(debug.Stack())) + t.FailNow() + } +} + +func AssertNotNil(t *testing.T, key string, v any) { + if v == nil { + t.Errorf("AssertNotNil(%s) failed", key) + t.Error(string(debug.Stack())) + t.FailNow() + } +} + func unpointer(v any) any { if v == nil { return v diff --git a/scnserver/test/util/structure.go b/scnserver/test/util/structure.go index d07fa15..7766db0 100644 --- a/scnserver/test/util/structure.go +++ b/scnserver/test/util/structure.go @@ -125,6 +125,18 @@ func assertjsonStructureMatchSingleValue(t *testing.T, strschema string, realVal t.Errorf("Key < %s > is not a int (its actually %T: '%v')", keyPath, realValue, realValue) return } + case "object|null": + if langext.IsNil(realValue) { + return // OK + } + if _, ok := realValue.(map[string]any); !ok { + t.Errorf("Key < %s > is not an object|null (its actually %T: '%v')", keyPath, realValue, realValue) + return + } + if _, err := time.Parse(time.RFC3339, realValue.(string)); err != nil { + t.Errorf("Key < %s > is not an object|null (its '%v')", keyPath, realValue) + return + } default: t.Errorf("Unknown schema type '%s' for key < %s >", strschema, keyPath) return