Fix SQL unmarshalling of optional nested structs (LEFT JOIN)

This commit is contained in:
Mike Schwörer 2022-12-22 12:43:40 +01:00
parent 0cb2a977a0
commit 0112d681ac
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
20 changed files with 403 additions and 135 deletions

View File

@ -17,6 +17,8 @@
- diff my currently used scnsend script vs the one in the docs here - diff my currently used scnsend script vs the one in the docs here
- Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions
------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------
- in my script: use (backupname || hostname) for sendername - in my script: use (backupname || hostname) for sendername

View File

@ -18,6 +18,7 @@ const (
BINDFAIL_QUERY_PARAM APIError = 1151 BINDFAIL_QUERY_PARAM APIError = 1151
BINDFAIL_BODY_PARAM APIError = 1152 BINDFAIL_BODY_PARAM APIError = 1152
BINDFAIL_URI_PARAM APIError = 1153 BINDFAIL_URI_PARAM APIError = 1153
INVALID_BODY_PARAM APIError = 1161
INVALID_ENUM_VALUE APIError = 1171 INVALID_ENUM_VALUE APIError = 1171
NO_TITLE APIError = 1201 NO_TITLE APIError = 1201
@ -29,12 +30,13 @@ const (
CHANNEL_TOO_LONG APIError = 1207 CHANNEL_TOO_LONG APIError = 1207
CHANNEL_NAME_WOULD_CHANGE APIError = 1207 CHANNEL_NAME_WOULD_CHANGE APIError = 1207
USER_NOT_FOUND APIError = 1301 USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302 CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303 CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304 SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305 MESSAGE_NOT_FOUND APIError = 1305
USER_AUTH_FAILED APIError = 1311 SUBSCRIPTION_USER_MISMATCH APIError = 1306
USER_AUTH_FAILED APIError = 1311
NO_DEVICE_LINKED APIError = 1401 NO_DEVICE_LINKED APIError = 1401

View File

@ -2,7 +2,6 @@ package handler
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db" "blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/db/cursortoken" "blackforestbytes.com/simplecloudnotifier/db/cursortoken"
@ -501,8 +500,8 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// @ID api-channels-list // @ID api-channels-list
// @Tags API-v2 // @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any) // @Param selector query string false "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
// //
// @Success 200 {object} handler.ListChannels.response // @Success 200 {object} handler.ListChannels.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid" // @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
@ -667,6 +666,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
if b.Name == "" {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil)
}
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name) channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name) channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
@ -677,17 +680,17 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
user, err := h.database.GetUser(ctx, u.UserID) user, err := h.database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", nil) return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
} }
if err != nil { if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
} }
if len(channelDisplayName) > user.MaxChannelNameLength() { if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
} }
if len(channelInternalName) > user.MaxChannelNameLength() { if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil) return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
} }
if channelExisting != nil { if channelExisting != nil {
@ -915,27 +918,27 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
// ListUserSubscriptions swaggerdoc // ListUserSubscriptions swaggerdoc
// //
// @Summary List all subscriptions of a user (incoming/owned) // @Summary List all subscriptions of a user (incoming/owned)
// // @Description The possible values for 'selector' are: // @Description The possible values for 'selector' are:
// // @Description - "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels) // @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
// // @Description - "owner_confirmed" Confirmed subscriptions with the user as owner // @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
// // @Description - "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner // @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
// // @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests) // @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
// // @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user // @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
// // @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests) // @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
// //
// @ID api-user-subscriptions-list
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @ID api-user-subscriptions-list
// @Param selector query string true "Filter subscribptions (default: owner_all)" Enums(owner_all, owner_confirmed, owner_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed) // @Tags API-v2
// //
// @Success 200 {object} handler.ListUserSubscriptions.response // @Param uid path int true "UserID"
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid" // @Param selector query string true "Filter subscriptions (default: owner_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/users/{uid}/subscriptions [GET] // @Success 200 {object} handler.ListUserSubscriptions.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/users/{uid}/subscriptions [GET]
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
@ -964,48 +967,48 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
var res []models.Subscription var res []models.Subscription
var err error var err error
if sel == "owner_all" { if sel == "outgoing_all" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "owner_confirmed" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "owner_unconfirmed" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_all" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil) res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
} }
} else if sel == "incoming_confirmed" { } else if sel == "outgoing_confirmed" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true)) res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
} }
} else if sel == "incoming_unconfirmed" { } else if sel == "outgoing_unconfirmed" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false)) res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
} }
} else if sel == "incoming_all" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_confirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_unconfirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else { } else {
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil) return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
@ -1111,9 +1114,8 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
} }
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
if subscription.SubscriberUserID != u.UserID { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON()))
@ -1159,9 +1161,8 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
} }
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
} }
err = h.database.DeleteSubscription(ctx, u.SubscriptionID) err = h.database.DeleteSubscription(ctx, u.SubscriptionID)
@ -1174,27 +1175,29 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// CreateSubscription swaggerdoc // CreateSubscription swaggerdoc
// //
// @Summary Creare/Request a subscription // @Summary Create/Request a subscription
// @ID api-subscriptions-create // @Description Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body
// @Tags API-v2 // @ID api-subscriptions-create
// @Tags API-v2
// //
// @Param uid path int true "UserID" // @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " " // @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " " // @Param post_data body handler.CreateSubscription.body false " "
// //
// @Success 200 {object} models.SubscriptionJSON // @Success 200 {object} models.SubscriptionJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid" // @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions" // @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error" // @Failure 500 {object} ginresp.apiError "internal server error"
// //
// @Router /api/users/{uid}/subscriptions [POST] // @Router /api/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct { type uri struct {
UserID models.UserID `uri:"uid"` UserID models.UserID `uri:"uid"`
} }
type body struct { type body struct {
ChannelOwnerUserID models.UserID `form:"channel_owner_user_id" binding:"required"` ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id"`
Channel string `form:"channel_name" binding:"required"` ChannelInternalName *string `json:"channel_internal_name"`
ChannelID *models.ChannelID `json:"channel_id"`
} }
type query struct { type query struct {
ChanSubscribeKey *string `json:"chan_subscribe_key" form:"chan_subscribe_key"` ChanSubscribeKey *string `json:"chan_subscribe_key" form:"chan_subscribe_key"`
@ -1213,21 +1216,45 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return *permResp return *permResp
} }
channelInternalName := h.app.NormalizeChannelInternalName(b.Channel) var channel models.Channel
if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil {
channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName)
outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil)
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if channel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
} }
if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) { if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID) sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID)
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
} }
@ -1279,9 +1306,8 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil { if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err) return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
} }
if subscription.SubscriberUserID != u.UserID {
if subscription.ChannelOwnerUserID != u.UserID { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
if b.Confirmed != nil { if b.Confirmed != nil {

View File

@ -124,7 +124,7 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
_, libVersionNumber, _ := sqlite3.Version() _, libVersionNumber, _ := sqlite3.Version()
if libVersionNumber < 3039000 { if libVersionNumber < 3039000 {
ginresp.InternalError(errors.New("sqlite version too low")) return ginresp.InternalError(errors.New("sqlite version too low"))
} }
err := h.app.Database.Ping(ctx) err := h.app.Database.Ping(ctx)

View File

@ -57,6 +57,30 @@ func (db *Database) GetChannelByNameAndSendKey(ctx TxContext, chanName string, s
return &channel, nil return &channel, nil
} }
func (db *Database) GetChannelByID(ctx TxContext, chanid models.ChannelID) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.Query(ctx, "SELECT * FROM channels WHERE channel_id = :cid LIMIT 1", sq.PP{
"cid": chanid,
})
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) { func (db *Database) CreateChannel(ctx TxContext, userid models.UserID, dispName string, intName string, subscribeKey string, sendKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {

View File

@ -206,12 +206,7 @@ func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string)
} }
type res struct { type res struct {
CID int64 `db:"cid"` Name string `db:"name"`
Name string `db:"name"`
Type string `db:"type"`
NotNull int `db:"notnull"`
DFLT *string `db:"dflt_value"`
PK int `db:"pk"`
} }
rows, err := pp.db.Query(ctx, "PRAGMA table_info('"+tablename+"');", sq.PP{}) rows, err := pp.db.Query(ctx, "PRAGMA table_info('"+tablename+"');", sq.PP{})
@ -219,7 +214,7 @@ func (pp *DBPreprocessor) getTableColumns(ctx context.Context, tablename string)
return nil, err return nil, err
} }
resrows, err := sq.ScanAll[res](rows, true) resrows, err := sq.ScanAll[res](rows, sq.SModeFast, sq.Unsafe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -62,7 +62,7 @@ func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID models.C
return data, nil return data, nil
} }
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) { func (db *Database) ListSubscriptionsByChannelOwner(ctx TxContext, ownerUserID models.UserID, confirmed *bool) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db) tx, err := ctx.GetOrCreateTransaction(db)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -8,7 +8,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.28.0 github.com/rs/zerolog v1.28.0
github.com/swaggo/swag v1.8.7 github.com/swaggo/swag v1.8.7
gogs.mikescher.com/BlackForestBytes/goext v0.0.42 gogs.mikescher.com/BlackForestBytes/goext v0.0.46
) )
require ( require (

View File

@ -122,6 +122,12 @@ gogs.mikescher.com/BlackForestBytes/goext v0.0.41 h1:3p/MtkHZ2gulSdizXql3VnFf2v7
gogs.mikescher.com/BlackForestBytes/goext v0.0.41/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g= gogs.mikescher.com/BlackForestBytes/goext v0.0.41/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.42 h1:u6+pDRrL9wSvJG7gVsGUO4dA54qzac5LsqoXqi6oo9E= gogs.mikescher.com/BlackForestBytes/goext v0.0.42 h1:u6+pDRrL9wSvJG7gVsGUO4dA54qzac5LsqoXqi6oo9E=
gogs.mikescher.com/BlackForestBytes/goext v0.0.42/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g= gogs.mikescher.com/BlackForestBytes/goext v0.0.42/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.44 h1:YC8SrQk1BEDR5wCdLZ2trnNvkUg/sssW94XYKrsKyc4=
gogs.mikescher.com/BlackForestBytes/goext v0.0.44/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.45 h1:1naABIgSa5hhWPT7kYAAEeIUBNLo7nVvE6/kz9LoY9Q=
gogs.mikescher.com/BlackForestBytes/goext v0.0.45/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
gogs.mikescher.com/BlackForestBytes/goext v0.0.46 h1:7nV9RKnnz/qgkVWvlj4MOAITbe+Gas1niVQgvbHnNk8=
gogs.mikescher.com/BlackForestBytes/goext v0.0.46/go.mod h1:/u9JtMwCP68ix4R9BJ/MT0Lm+QScmqIoyYZFKBGzv9g=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=

View File

@ -117,7 +117,7 @@ func (c ChannelWithSubscriptionDB) Model() ChannelWithSubscription {
} }
func DecodeChannel(r *sqlx.Rows) (Channel, error) { func DecodeChannel(r *sqlx.Rows) (Channel, error) {
data, err := sq.ScanSingle[ChannelDB](r, true) data, err := sq.ScanSingle[ChannelDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return Channel{}, err return Channel{}, err
} }
@ -125,7 +125,7 @@ func DecodeChannel(r *sqlx.Rows) (Channel, error) {
} }
func DecodeChannels(r *sqlx.Rows) ([]Channel, error) { func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
data, err := sq.ScanAll[ChannelDB](r, true) data, err := sq.ScanAll[ChannelDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -133,7 +133,7 @@ func DecodeChannels(r *sqlx.Rows) ([]Channel, error) {
} }
func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error) { func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error) {
data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, true) data, err := sq.ScanSingle[ChannelWithSubscriptionDB](r, sq.SModeExtended, sq.Safe, true)
if err != nil { if err != nil {
return ChannelWithSubscription{}, err return ChannelWithSubscription{}, err
} }
@ -141,7 +141,7 @@ func DecodeChannelWithSubscription(r *sqlx.Rows) (ChannelWithSubscription, error
} }
func DecodeChannelsWithSubscription(r *sqlx.Rows) ([]ChannelWithSubscription, error) { func DecodeChannelsWithSubscription(r *sqlx.Rows) ([]ChannelWithSubscription, error) {
data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, true) data, err := sq.ScanAll[ChannelWithSubscriptionDB](r, sq.SModeExtended, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -69,7 +69,7 @@ func (c ClientDB) Model() Client {
} }
func DecodeClient(r *sqlx.Rows) (Client, error) { func DecodeClient(r *sqlx.Rows) (Client, error) {
data, err := sq.ScanSingle[ClientDB](r, true) data, err := sq.ScanSingle[ClientDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return Client{}, err return Client{}, err
} }
@ -77,7 +77,7 @@ func DecodeClient(r *sqlx.Rows) (Client, error) {
} }
func DecodeClients(r *sqlx.Rows) ([]Client, error) { func DecodeClients(r *sqlx.Rows) ([]Client, error) {
data, err := sq.ScanAll[ClientDB](r, true) data, err := sq.ScanAll[ClientDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -89,7 +89,7 @@ func (d DeliveryDB) Model() Delivery {
} }
func DecodeDelivery(r *sqlx.Rows) (Delivery, error) { func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
data, err := sq.ScanSingle[DeliveryDB](r, true) data, err := sq.ScanSingle[DeliveryDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return Delivery{}, err return Delivery{}, err
} }
@ -97,7 +97,7 @@ func DecodeDelivery(r *sqlx.Rows) (Delivery, error) {
} }
func DecodeDeliveries(r *sqlx.Rows) ([]Delivery, error) { func DecodeDeliveries(r *sqlx.Rows) ([]Delivery, error) {
data, err := sq.ScanAll[DeliveryDB](r, true) data, err := sq.ScanAll[DeliveryDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -146,7 +146,7 @@ func (m MessageDB) Model() Message {
} }
func DecodeMessage(r *sqlx.Rows) (Message, error) { func DecodeMessage(r *sqlx.Rows) (Message, error) {
data, err := sq.ScanSingle[MessageDB](r, true) data, err := sq.ScanSingle[MessageDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return Message{}, err return Message{}, err
} }
@ -154,7 +154,7 @@ func DecodeMessage(r *sqlx.Rows) (Message, error) {
} }
func DecodeMessages(r *sqlx.Rows) ([]Message, error) { func DecodeMessages(r *sqlx.Rows) ([]Message, error) {
data, err := sq.ScanAll[MessageDB](r, true) data, err := sq.ScanAll[MessageDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -62,7 +62,7 @@ func (s SubscriptionDB) Model() Subscription {
} }
func DecodeSubscription(r *sqlx.Rows) (Subscription, error) { func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
data, err := sq.ScanSingle[SubscriptionDB](r, true) data, err := sq.ScanSingle[SubscriptionDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return Subscription{}, err return Subscription{}, err
} }
@ -70,7 +70,7 @@ func DecodeSubscription(r *sqlx.Rows) (Subscription, error) {
} }
func DecodeSubscriptions(r *sqlx.Rows) ([]Subscription, error) { func DecodeSubscriptions(r *sqlx.Rows) ([]Subscription, error) {
data, err := sq.ScanAll[SubscriptionDB](r, true) data, err := sq.ScanAll[SubscriptionDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -163,7 +163,7 @@ func (u UserDB) Model() User {
} }
func DecodeUser(r *sqlx.Rows) (User, error) { func DecodeUser(r *sqlx.Rows) (User, error) {
data, err := sq.ScanSingle[UserDB](r, true) data, err := sq.ScanSingle[UserDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return User{}, err return User{}, err
} }
@ -171,7 +171,7 @@ func DecodeUser(r *sqlx.Rows) (User, error) {
} }
func DecodeUsers(r *sqlx.Rows) ([]User, error) { func DecodeUsers(r *sqlx.Rows) ([]User, error) {
data, err := sq.ScanAll[UserDB](r, true) data, err := sq.ScanAll[UserDB](r, sq.SModeFast, sq.Safe, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1176,8 +1176,7 @@
"type": "string", "type": "string",
"description": "Filter channels (default: owned)", "description": "Filter channels (default: owned)",
"name": "selector", "name": "selector",
"in": "query", "in": "query"
"required": true
} }
], ],
"responses": { "responses": {
@ -1743,6 +1742,7 @@
}, },
"/api/users/{uid}/subscriptions": { "/api/users/{uid}/subscriptions": {
"get": { "get": {
"description": "The possible values for 'selector' are:\n- \"owner_all\" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)\n- \"owner_confirmed\" Confirmed subscriptions with the user as owner\n- \"owner_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as owner\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)",
"tags": [ "tags": [
"API-v2" "API-v2"
], ],
@ -1766,7 +1766,7 @@
"incoming_unconfirmed" "incoming_unconfirmed"
], ],
"type": "string", "type": "string",
"description": "Filter subscribptions (default: owner_all)", "description": "Filter subscriptions (default: owner_all)",
"name": "selector", "name": "selector",
"in": "query", "in": "query",
"required": true "required": true
@ -1800,10 +1800,11 @@
} }
}, },
"post": { "post": {
"description": "Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body",
"tags": [ "tags": [
"API-v2" "API-v2"
], ],
"summary": "Creare/Request a subscription", "summary": "Create/Request a subscription",
"operationId": "api-subscriptions-create", "operationId": "api-subscriptions-create",
"parameters": [ "parameters": [
{ {
@ -2410,11 +2411,15 @@
"handler.CreateSubscription.body": { "handler.CreateSubscription.body": {
"type": "object", "type": "object",
"required": [ "required": [
"channel", "channel_id",
"channel_internal_name",
"channel_owner_user_id" "channel_owner_user_id"
], ],
"properties": { "properties": {
"channel": { "channel_id": {
"type": "integer"
},
"channel_internal_name": {
"type": "string" "type": "string"
}, },
"channel_owner_user_id": { "channel_owner_user_id": {

View File

@ -60,12 +60,15 @@ definitions:
type: object type: object
handler.CreateSubscription.body: handler.CreateSubscription.body:
properties: properties:
channel: channel_id:
type: integer
channel_internal_name:
type: string type: string
channel_owner_user_id: channel_owner_user_id:
type: integer type: integer
required: required:
- channel - channel_id
- channel_internal_name
- channel_owner_user_id - channel_owner_user_id
type: object type: object
handler.CreateUser.body: handler.CreateUser.body:
@ -1315,7 +1318,6 @@ paths:
- all_any - all_any
in: query in: query
name: selector name: selector
required: true
type: string type: string
responses: responses:
"200": "200":
@ -1697,6 +1699,14 @@ paths:
- API-v2 - API-v2
/api/users/{uid}/subscriptions: /api/users/{uid}/subscriptions:
get: get:
description: |-
The possible values for 'selector' are:
- "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)
- "owner_confirmed" Confirmed subscriptions with the user as owner
- "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner
- "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
- "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
- "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
operationId: api-user-subscriptions-list operationId: api-user-subscriptions-list
parameters: parameters:
- description: UserID - description: UserID
@ -1704,7 +1714,7 @@ paths:
name: uid name: uid
required: true required: true
type: integer type: integer
- description: 'Filter subscribptions (default: owner_all)' - description: 'Filter subscriptions (default: owner_all)'
enum: enum:
- owner_all - owner_all
- owner_confirmed - owner_confirmed
@ -1737,6 +1747,8 @@ paths:
tags: tags:
- API-v2 - API-v2
post: post:
description: Either [channel_owner_user_id, channel_internal_name] or [channel_id]
must be supplied in the request body
operationId: api-subscriptions-create operationId: api-subscriptions-create
parameters: parameters:
- description: UserID - description: UserID
@ -1769,7 +1781,7 @@ paths:
description: internal server error description: internal server error
schema: schema:
$ref: '#/definitions/ginresp.apiError' $ref: '#/definitions/ginresp.apiError'
summary: Creare/Request a subscription summary: Create/Request a subscription
tags: tags:
- API-v2 - API-v2
/api/users/{uid}/subscriptions/{sid}: /api/users/{uid}/subscriptions/{sid}:

View File

@ -161,10 +161,10 @@ func TestListChannelsOwned(t *testing.T) {
} }
testdata := map[int][]string{ testdata := map[int][]string{
0: {"main", "chattingchamber", "unicdhll", "promotions", "reminders"}, 0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"}, 1: {"main", "private"},
2: {"main", "ü", "ö", "ä"}, 2: {"main", "ü", "ö", "ä"},
3: {"main", "innovations", "reminders"}, 3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"}, 4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"}, 5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"}, 6: {"main", "security", "lipsum"},
@ -175,8 +175,8 @@ func TestListChannelsOwned(t *testing.T) {
11: {"promotions"}, 11: {"promotions"},
12: {}, 12: {},
13: {}, 13: {},
14: {"", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"", "chan_other_nosub", "chan_other_request", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
} }
for k, v := range testdata { for k, v := range testdata {
@ -186,19 +186,143 @@ func TestListChannelsOwned(t *testing.T) {
} }
func TestListChannelsSubscribedAny(t *testing.T) { func TestListChannelsSubscribedAny(t *testing.T) {
t.SkipNow() //TODO ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
type chanlist struct {
Channels []gin.H `json:"channels"`
}
testdata := map[int][]string{
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
for k, v := range testdata {
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
}
} }
func TestListChannelsAllAny(t *testing.T) { func TestListChannelsAllAny(t *testing.T) {
t.SkipNow() //TODO ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
type chanlist struct {
Channels []gin.H `json:"channels"`
}
testdata := map[int][]string{
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
for k, v := range testdata {
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
}
} }
func TestListChannelsSubscribed(t *testing.T) { func TestListChannelsSubscribed(t *testing.T) {
t.SkipNow() //TODO ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
type chanlist struct {
Channels []gin.H `json:"channels"`
}
testdata := map[int][]string{
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
for k, v := range testdata {
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
}
} }
func TestListChannelsAll(t *testing.T) { func TestListChannelsAll(t *testing.T) {
t.SkipNow() //TODO ws, baseUrl, stop := tt.StartSimpleWebserver(t)
defer stop()
data := tt.InitDefaultData(t, ws)
type chanlist struct {
Channels []gin.H `json:"channels"`
}
testdata := map[int][]string{
0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"},
1: {"main", "private"},
2: {"main", "ü", "ö", "ä"},
3: {"main", "\U0001f5ff", "innovations", "reminders"},
4: {"main"},
5: {"main", "test1", "test2", "test3", "test4", "test5"},
6: {"main", "security", "lipsum"},
7: {"main"},
8: {"main"},
9: {"main", "manual@chan"},
10: {"main"},
11: {"promotions"},
12: {},
13: {},
14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases
15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases
}
for k, v := range testdata {
r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID))
tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name")
}
} }
//TODO test missing channel-xx methods //TODO test missing channel-xx methods

View File

@ -257,8 +257,8 @@ var messageExamples = []msgex{
{11, "Promotions", "localhost", P2, SKEY, "New Product Launch: Introducing Our Latest Innovation", "We are excited to announce the release of our newest product, designed to revolutionize the industry. Don't miss out on this game-changing technology.", timeext.FromHours(-12.21)}, {11, "Promotions", "localhost", P2, SKEY, "New Product Launch: Introducing Our Latest Innovation", "We are excited to announce the release of our newest product, designed to revolutionize the industry. Don't miss out on this game-changing technology.", timeext.FromHours(-12.21)},
{11, "Promotions", "#S0", P0, SKEY, "Limited Time Offer: Get 50% Off Your Next Purchase", "For a limited time, take advantage of our special offer and get half off your next purchase. Don't miss out on this amazing deal.", 0}, {11, "Promotions", "#S0", P0, SKEY, "Limited Time Offer: Get 50% Off Your Next Purchase", "For a limited time, take advantage of our special offer and get half off your next purchase. Don't miss out on this amazing deal.", 0},
{11, "Promotions", "#S0", P2, SKEY, "Customer Appreciation Sale: Save Up to 75% on Your Favorite Products", "", 0}, {11, "Promotions", "#S0", P2, SKEY, "Customer Appreciation Sale: Save Up to 75% on Your Favorite Products", "", 0},
{11, "Promotions", "", P0, SKEY, "Sign Up for Our Newsletter and Save 10% on Your Next Order", "", 0}, {11, " Promotions", "", P0, SKEY, "Sign Up for Our Newsletter and Save 10% on Your Next Order", "", 0},
{11, "Promotions", "", PX, AKEY, "New Arrivals: Check Out Our Latest Collection", "We've just added new items to our collection and we think you'll love them. Take a look and see what's new in fashion, home decor, and more.", 0}, {11, "Promotions ", "", PX, AKEY, "New Arrivals: Check Out Our Latest Collection", "We've just added new items to our collection and we think you'll love them. Take a look and see what's new in fashion, home decor, and more.", 0},
{11, "Promotions", "", PX, AKEY, "Join Our Rewards Program and Earn Points on Every Purchase", "Sign up for our rewards program and earn points on every purchase you make. Redeem your points for discounts, free products, and more.", 0}, {11, "Promotions", "", PX, AKEY, "Join Our Rewards Program and Earn Points on Every Purchase", "Sign up for our rewards program and earn points on every purchase you make. Redeem your points for discounts, free products, and more.", 0},
{11, "Promotions", "#S0", P0, SKEY, "Seasonal Special: Save on Your Favorite Fall Products", "As the leaves change color and the air gets cooler, we have the perfect products to help you enjoy the season. Take advantage of our special offers and save on your favorite fall products.", 0}, {11, "Promotions", "#S0", P0, SKEY, "Seasonal Special: Save on Your Favorite Fall Products", "As the leaves change color and the air gets cooler, we have the perfect products to help you enjoy the season. Take advantage of our special offers and save on your favorite fall products.", 0},
{11, "Promotions", "192.168.0.1", P1, AKEY, "Refer a Friend and Save on Your Next Order", "Share the love and refer a friend to our store. When they make a purchase, you'll receive a discount on your next order. It's a win-win for both of you.", 0}, {11, "Promotions", "192.168.0.1", P1, AKEY, "Refer a Friend and Save on Your Next Order", "Share the love and refer a friend to our store. When they make a purchase, you'll receive a discount on your next order. It's a win-win for both of you.", 0},
@ -384,10 +384,10 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
// Sub/Unsub for Users 12+13 // Sub/Unsub for Users 12+13
{ {
//TODO User 12 unsubscribe from 12:chan_self_unsub doUnsubscribe(t, baseUrl, users[14], users[14], "chan_self_unsub")
//TODO User 13 request-subscribe to 13:chan_other_request doSubscribe(t, baseUrl, users[14], users[15], "chan_other_request")
//TODO User 13 request-subscribe to 13:chan_other_accepted doSubscribe(t, baseUrl, users[14], users[15], "chan_other_accepted")
//TODO User 13 accept subscription from user 12 to 13:chan_other_accepted doAcceptSub(t, baseUrl, users[15], users[14], "chan_other_accepted")
} }
success = true success = true
@ -395,6 +395,78 @@ func InitDefaultData(t *testing.T, ws *logic.Application) DefData {
return DefData{User: users} return DefData{User: users}
} }
func doSubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
if user == chanOwner {
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", user.UID), gin.H{
"channel_owner_user_id": chanOwner.UID,
"channel_internal_name": chanInternalName,
})
} else {
type chanlist struct {
Channels []gin.H `json:"channels"`
}
clist := RequestAuthGet[chanlist](t, chanOwner.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=owned", chanOwner.UID))
var chandat gin.H
for _, v := range clist.Channels {
if v["internal_name"].(string) == chanInternalName {
chandat = v
break
}
}
RequestAuthPost[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?chan_subscribe_key=%s", user.UID, chandat["subscribe_key"].(string)), gin.H{
"channel_id": chandat["channel_id"].(float64),
})
}
}
func doUnsubscribe(t *testing.T, baseUrl string, user Userdat, chanOwner Userdat, chanInternalName string) {
type chanlist struct {
Subscriptions []gin.H `json:"subscriptions"`
}
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?selector=outgoing_confirmed", user.UID))
var subdat gin.H
for _, v := range slist.Subscriptions {
if v["channel_internal_name"].(string) == chanInternalName && int64(v["channel_owner_user_id"].(float64)) == chanOwner.UID {
subdat = v
break
}
}
RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{})
}
func doAcceptSub(t *testing.T, baseUrl string, user Userdat, subscriber Userdat, chanInternalName string) {
type chanlist struct {
Subscriptions []gin.H `json:"subscriptions"`
}
slist := RequestAuthGet[chanlist](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions?selector=incoming_unconfirmed", user.UID))
var subdat gin.H
for _, v := range slist.Subscriptions {
if v["channel_internal_name"].(string) == chanInternalName && int64(v["subscriber_user_id"].(float64)) == subscriber.UID {
subdat = v
break
}
}
RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{})
}
func Lipsum(seed int64, paracount int) string { func Lipsum(seed int64, paracount int) string {
return loremipsum.NewWithSeed(seed).Paragraphs(paracount) return loremipsum.NewWithSeed(seed).Paragraphs(paracount)
} }

View File

@ -152,7 +152,7 @@ func RequestAny[TResult any](t *testing.T, akey string, method string, baseURL s
TPrintln("") TPrintln("")
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
TestFail(t, "Statuscode != 200") TestFailFmt(t, "Statuscode != 200 (actual = %d)", resp.StatusCode)
} }
var data TResult var data TResult