SMQ-2761 - Add route field to channels (#2772)

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
Felix Gateru
2025-05-21 19:53:40 +03:00
committed by GitHub
parent 7f4633a3d1
commit 04f6117e36
14 changed files with 363 additions and 41 deletions
+17 -22
View File
@@ -454,7 +454,7 @@ paths:
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/{domainID}/channels/{chanID}/connect:
post:
operationId: connectClientsToChannel
@@ -534,14 +534,14 @@ components:
type: string
example: channelName
description: Free-form channel name. Channel name is unique on the given hierarchy level.
description:
type: string
example: long channel description
description: Channel description, free form text.
parent_id:
type: string
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Id of parent channel, it must be existing channel.
route:
type: string
example: channelRoute
description: Channel route.
metadata:
type: object
example: { "location": "example" }
@@ -557,7 +557,7 @@ components:
ParentGroupReqObj:
type: object
properties:
parent_group_id:
parent_group_id:
type: string
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
@@ -587,10 +587,10 @@ components:
format: uuid
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
description: Channel parent identifier.
description:
route:
type: string
example: long channel description
description: Channel description, free form text.
example: channelRoute
description: Channel route.
metadata:
type: object
example: { "role": "general" }
@@ -655,10 +655,6 @@ components:
type: string
example: channelName
description: Free-form channel name. Channel name is unique on the given hierarchy level.
description:
type: string
example: long description but not too long
description: Channel description, free form text.
metadata:
type: object
example: { "role": "general" }
@@ -666,8 +662,7 @@ components:
required:
- name
- metadata
- description
ChannelUpdateTags:
type: object
properties:
@@ -817,7 +812,7 @@ components:
format: uuid
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
Metadata:
name: metadata
description: Metadata filter. Filtering is performed matching the parameter with metadata on top level. Parameter is json.
@@ -867,7 +862,7 @@ components:
application/json:
schema:
$ref: "#/components/schemas/ChannelReqObj"
ChannelsCreateReq:
description: JSON-formatted document describing the new channels to be registered
required: true
@@ -885,15 +880,15 @@ components:
application/json:
schema:
$ref: "#/components/schemas/ChannelUpdate"
ChannelUpdateTagsReq:
description: JSON-formated document describing the tags of channel to be updated.
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ChannelUpdate"
$ref: "#/components/schemas/ChannelUpdateTags"
ChannelParentGroupReq:
description: JSON-formated document describing the parent group to be set to or removed from a channel.
required: true
@@ -909,7 +904,7 @@ components:
application/json:
schema:
$ref: "#/components/schemas/ConnectionReqSchema"
ChannelConnReq:
description: JSON-formatted document describing the new connection.
required: true
@@ -976,7 +971,7 @@ components:
operationId: unassignGroupsFromChannel
parameters:
chanID: $response.body#/id
ChannelsCreateRes:
description: Registered new channels.
headers:
+63
View File
@@ -73,6 +73,8 @@ func TestCreateChannelEndpoint(t *testing.T) {
"name": "test",
},
}
reqWithRoute := reqChannel
reqWithRoute.Route = valid
cases := []struct {
desc string
@@ -97,6 +99,16 @@ func TestCreateChannelEndpoint(t *testing.T) {
status: http.StatusCreated,
err: nil,
},
{
desc: "create channel with route",
token: validToken,
domainID: validID,
req: reqWithRoute,
contentType: contentType,
svcResp: []channels.Channel{validChannelResp},
status: http.StatusCreated,
err: nil,
},
{
desc: "create channel with invalid token",
token: invalidToken,
@@ -140,6 +152,29 @@ func TestCreateChannelEndpoint(t *testing.T) {
status: http.StatusBadRequest,
err: apiutil.ErrNameSize,
},
{
desc: "create channel with invalid route format",
token: validToken,
domainID: validID,
req: channels.Channel{
Name: valid,
Route: "__invalid",
},
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrInvalidRouteFormat,
},
{
desc: "create channel with UUID route",
token: validToken, domainID: validID,
req: channels.Channel{
Name: valid,
Route: testsutil.GenerateUUID(t),
},
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrInvalidRouteFormat,
},
{
desc: "create channel with invalid content type",
token: validToken,
@@ -205,6 +240,7 @@ func TestCreateChannelsEndpoint(t *testing.T) {
Metadata: map[string]interface{}{
"name": "test",
},
Route: valid,
},
}
@@ -276,6 +312,33 @@ func TestCreateChannelsEndpoint(t *testing.T) {
status: http.StatusBadRequest,
err: apiutil.ErrNameSize,
},
{
desc: "create channels with invalid route format",
token: validToken,
domainID: validID,
req: []channels.Channel{
{
Name: valid,
Route: "__invalid",
},
},
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrInvalidRouteFormat,
},
{
desc: "create channel with UUID route",
token: validToken, domainID: validID,
req: []channels.Channel{
{
Name: valid,
Route: testsutil.GenerateUUID(t),
},
},
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrInvalidRouteFormat,
},
{
desc: "create channels with invalid content type",
token: validToken,
+16
View File
@@ -25,6 +25,14 @@ func (req createChannelReq) validate() error {
return apiutil.ErrMissingChannelID
}
}
if req.Channel.Route != "" {
if err := api.ValidateRoute(req.Channel.Route); err != nil {
return err
}
if err := api.ValidateUUID(req.Channel.Route); err == nil {
return apiutil.ErrInvalidRouteFormat
}
}
return nil
}
@@ -46,6 +54,14 @@ func (req createChannelsReq) validate() error {
if len(channel.Name) > api.MaxNameSize {
return apiutil.ErrNameSize
}
if channel.Route != "" {
if err := api.ValidateRoute(channel.Route); err != nil {
return err
}
if err := api.ValidateUUID(channel.Route); err == nil {
return apiutil.ErrInvalidRouteFormat
}
}
}
return nil
+28 -4
View File
@@ -26,7 +26,8 @@ func TestCreateChannelReqValidation(t *testing.T) {
desc: "valid request",
req: createChannelReq{
Channel: channels.Channel{
Name: valid,
Name: valid,
Route: valid,
},
},
err: nil,
@@ -35,11 +36,32 @@ func TestCreateChannelReqValidation(t *testing.T) {
desc: "long name",
req: createChannelReq{
Channel: channels.Channel{
Name: strings.Repeat("a", api.MaxNameSize+1),
Name: strings.Repeat("a", api.MaxNameSize+1),
Route: valid,
},
},
err: apiutil.ErrNameSize,
},
{
desc: "invalid route",
req: createChannelReq{
Channel: channels.Channel{
Name: valid,
Route: "__invalid",
},
},
err: apiutil.ErrInvalidRouteFormat,
},
{
desc: "uuid as route",
req: createChannelReq{
Channel: channels.Channel{
Name: valid,
Route: testsutil.GenerateUUID(t),
},
},
err: apiutil.ErrInvalidRouteFormat,
},
{
desc: "missing channel ID",
req: createChannelReq{
@@ -68,7 +90,8 @@ func TestCreateChannelsReqValidation(t *testing.T) {
req: createChannelsReq{
Channels: []channels.Channel{
{
Name: valid,
Name: valid,
Route: valid,
},
},
},
@@ -79,7 +102,8 @@ func TestCreateChannelsReqValidation(t *testing.T) {
req: createChannelsReq{
Channels: []channels.Channel{
{
Name: strings.Repeat("a", api.MaxNameSize+1),
Name: strings.Repeat("a", api.MaxNameSize+1),
Route: valid,
},
},
},
+4
View File
@@ -24,6 +24,7 @@ type Channel struct {
Tags []string `json:"tags,omitempty"`
ParentGroup string `json:"parent_group_id,omitempty"`
Domain string `json:"domain_id,omitempty"`
Route string `json:"route,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
@@ -151,6 +152,9 @@ type Repository interface {
// RetrieveByID retrieves the channel having the provided identifier
RetrieveByID(ctx context.Context, id string) (Channel, error)
// RetrieveByRoute retrieves the channel having the provided route
RetrieveByRoute(ctx context.Context, route, domainID string) (Channel, error)
// RetrieveByIDWithRoles retrieves channel by its unique ID along with member roles.
RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (Channel, error)
+7
View File
@@ -53,6 +53,7 @@ func (cce createChannelEvent) Encode() (map[string]interface{}, error) {
"operation": channelCreate,
"id": cce.ID,
"roles_provisioned": cce.rolesProvisioned,
"route": cce.Route,
"status": cce.Status.String(),
"created_at": cce.CreatedAt,
"domain": cce.DomainID,
@@ -97,6 +98,9 @@ func (uce updateChannelEvent) Encode() (map[string]interface{}, error) {
if uce.ID != "" {
val["id"] = uce.ID
}
if uce.Route != "" {
val["route"] = uce.Route
}
if uce.Name != "" {
val["name"] = uce.Name
}
@@ -161,6 +165,9 @@ func (vce viewChannelEvent) Encode() (map[string]interface{}, error) {
if vce.Name != "" {
val["name"] = vce.Name
}
if vce.Route != "" {
val["route"] = vce.Route
}
if len(vce.Tags) > 0 {
val["tags"] = vce.Tags
}
+56
View File
@@ -1063,6 +1063,62 @@ func (_c *Repository_RetrieveByIDWithRoles_Call) RunAndReturn(run func(ctx conte
return _c
}
// RetrieveByRoute provides a mock function for the type Repository
func (_mock *Repository) RetrieveByRoute(ctx context.Context, route string, domainID string) (channels.Channel, error) {
ret := _mock.Called(ctx, route, domainID)
if len(ret) == 0 {
panic("no return value specified for RetrieveByRoute")
}
var r0 channels.Channel
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (channels.Channel, error)); ok {
return returnFunc(ctx, route, domainID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) channels.Channel); ok {
r0 = returnFunc(ctx, route, domainID)
} else {
r0 = ret.Get(0).(channels.Channel)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, route, domainID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_RetrieveByRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByRoute'
type Repository_RetrieveByRoute_Call struct {
*mock.Call
}
// RetrieveByRoute is a helper method to define mock.On call
// - ctx
// - route
// - domainID
func (_e *Repository_Expecter) RetrieveByRoute(ctx interface{}, route interface{}, domainID interface{}) *Repository_RetrieveByRoute_Call {
return &Repository_RetrieveByRoute_Call{Call: _e.mock.On("RetrieveByRoute", ctx, route, domainID)}
}
func (_c *Repository_RetrieveByRoute_Call) Run(run func(ctx context.Context, route string, domainID string)) *Repository_RetrieveByRoute_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *Repository_RetrieveByRoute_Call) Return(channel channels.Channel, err error) *Repository_RetrieveByRoute_Call {
_c.Call.Return(channel, err)
return _c
}
func (_c *Repository_RetrieveByRoute_Call) RunAndReturn(run func(ctx context.Context, route string, domainID string) (channels.Channel, error)) *Repository_RetrieveByRoute_Call {
_c.Call.Return(run)
return _c
}
// RetrieveEntitiesRolesActionsMembers provides a mock function for the type Repository
func (_mock *Repository) RetrieveEntitiesRolesActionsMembers(ctx context.Context, entityIDs []string) ([]roles.EntityActionRole, []roles.EntityMemberRole, error) {
ret := _mock.Called(ctx, entityIDs)
+44 -7
View File
@@ -58,9 +58,9 @@ func (cr *channelRepository) Save(ctx context.Context, chs ...channels.Channel)
dbchs = append(dbchs, dbch)
}
q := `INSERT INTO channels (id, name, tags, domain_id, parent_group_id, metadata, created_at, updated_at, updated_by, status)
VALUES (:id, :name, :tags, :domain_id, :parent_group_id, :metadata, :created_at, :updated_at, :updated_by, :status)
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`
q := `INSERT INTO channels (id, name, tags, domain_id, parent_group_id, route, metadata, created_at, updated_at, updated_by, status)
VALUES (:id, :name, :tags, :domain_id, :parent_group_id, :route, :metadata, :created_at, :updated_at, :updated_by, :status)
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`
row, err := cr.db.NamedQueryContext(ctx, q, dbchs)
if err != nil {
@@ -100,7 +100,7 @@ func (cr *channelRepository) Update(ctx context.Context, channel channels.Channe
}
q := fmt.Sprintf(`UPDATE channels SET %s updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id AND status = :status
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`,
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`,
upq)
channel.Status = channels.EnabledStatus
return cr.update(ctx, channel, q)
@@ -109,7 +109,7 @@ func (cr *channelRepository) Update(ctx context.Context, channel channels.Channe
func (cr *channelRepository) UpdateTags(ctx context.Context, channel channels.Channel) (channels.Channel, error) {
q := `UPDATE channels SET tags = :tags, updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id AND status = :status
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`
channel.Status = channels.EnabledStatus
return cr.update(ctx, channel, q)
}
@@ -117,13 +117,13 @@ func (cr *channelRepository) UpdateTags(ctx context.Context, channel channels.Ch
func (cr *channelRepository) ChangeStatus(ctx context.Context, channel channels.Channel) (channels.Channel, error) {
q := `UPDATE channels SET status = :status, updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, status, created_at, updated_at, updated_by`
RETURNING id, name, tags, metadata, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, status, created_at, updated_at, updated_by`
return cr.update(ctx, channel, q)
}
func (cr *channelRepository) RetrieveByID(ctx context.Context, id string) (channels.Channel, error) {
q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, metadata, created_at, updated_at, updated_by, status FROM channels WHERE id = :id`
q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, metadata, created_at, updated_at, updated_by, status FROM channels WHERE id = :id`
dbch := dbChannel{
ID: id,
@@ -146,6 +146,32 @@ func (cr *channelRepository) RetrieveByID(ctx context.Context, id string) (chann
return channels.Channel{}, repoerr.ErrNotFound
}
func (cr *channelRepository) RetrieveByRoute(ctx context.Context, route, domainID string) (channels.Channel, error) {
q := `SELECT id, name, tags, COALESCE(domain_id, '') AS domain_id, COALESCE(parent_group_id, '') AS parent_group_id, route, metadata, created_at, updated_at, updated_by, status
FROM channels WHERE route = :route AND domain_id = :domain_id`
dbch := dbChannel{
Route: toNullString(route),
Domain: domainID,
}
row, err := cr.db.NamedQueryContext(ctx, q, dbch)
if err != nil {
return channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer row.Close()
dbch = dbChannel{}
if row.Next() {
if err := row.StructScan(&dbch); err != nil {
return channels.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return toChannel(dbch)
}
return channels.Channel{}, repoerr.ErrNotFound
}
func (cr *channelRepository) RetrieveByIDWithRoles(ctx context.Context, id, memberID string) (channels.Channel, error) {
query := `
WITH selected_channel AS (
@@ -311,6 +337,7 @@ func (cr *channelRepository) RetrieveByIDWithRoles(ctx context.Context, id, memb
c2.tags,
COALESCE(c2.domain_id, '') AS domain_id,
COALESCE(c2.parent_group_id, '') AS parent_group_id,
c2.route,
c2.metadata,
c2.created_at,
c2.created_by,
@@ -381,6 +408,7 @@ func (cr *channelRepository) RetrieveAll(ctx context.Context, pm channels.Page)
c.metadata,
COALESCE(c.domain_id, '') AS domain_id,
COALESCE(parent_group_id, '') AS parent_group_id,
c.route,
COALESCE((SELECT path FROM groups WHERE id = c.parent_group_id), ''::::ltree) AS parent_group_path,
c.status,
c.created_by,
@@ -488,6 +516,7 @@ func (repo *channelRepository) retrieveChannels(ctx context.Context, domainID, u
c.name,
c.domain_id,
c.parent_group_id,
c.route,
c.tags,
c.metadata,
c.created_by,
@@ -546,6 +575,7 @@ func (repo *channelRepository) retrieveChannels(ctx context.Context, domainID, u
c.name,
c.domain_id,
c.parent_group_id,
c.route,
c.tags,
c.metadata,
c.created_by,
@@ -592,6 +622,7 @@ WITH direct_channels AS (
c.name,
c.domain_id,
c.parent_group_id,
c.route,
c.tags,
c.metadata,
c.created_by,
@@ -749,6 +780,7 @@ groups_channels AS (
c.name,
c.domain_id,
c.parent_group_id,
c.route,
c.tags,
c.metadata,
c.created_by,
@@ -780,6 +812,7 @@ final_channels AS (
gc."name",
gc.domain_id,
gc.parent_group_id,
gc.route,
gc.tags,
gc.metadata,
gc.created_by,
@@ -804,6 +837,7 @@ final_channels AS (
dc."name",
dc.domain_id,
dc.parent_group_id,
dc.route,
dc.tags,
dc.metadata,
dc.created_by,
@@ -1075,6 +1109,7 @@ type dbChannel struct {
ParentGroup sql.NullString `db:"parent_group_id,omitempty"`
Tags pgtype.TextArray `db:"tags,omitempty"`
Domain string `db:"domain_id"`
Route sql.NullString `db:"route,omitempty"`
Metadata []byte `db:"metadata,omitempty"`
CreatedBy *string `db:"created_by,omitempty"`
CreatedAt time.Time `db:"created_at,omitempty"`
@@ -1125,6 +1160,7 @@ func toDBChannel(ch channels.Channel) (dbChannel, error) {
Name: ch.Name,
ParentGroup: toNullString(ch.ParentGroup),
Domain: ch.Domain,
Route: toNullString(ch.Route),
Tags: tags,
Metadata: data,
CreatedBy: createdBy,
@@ -1198,6 +1234,7 @@ func toChannel(ch dbChannel) (channels.Channel, error) {
Name: ch.Name,
Tags: tags,
Domain: ch.Domain,
Route: toString(ch.Route),
ParentGroup: toString(ch.ParentGroup),
Metadata: metadata,
CreatedBy: createdBy,
+91
View File
@@ -29,6 +29,7 @@ var (
Domain: testsutil.GenerateUUID(&testing.T{}),
ParentGroup: testsutil.GenerateUUID(&testing.T{}),
Name: namegen.Generate(),
Route: testsutil.GenerateUUID(&testing.T{}),
Tags: []string{"tag1", "tag2"},
Metadata: map[string]interface{}{"key": "value"},
CreatedAt: time.Now().UTC().Truncate(time.Microsecond),
@@ -54,6 +55,19 @@ func TestSave(t *testing.T) {
duplicateChannelID := testsutil.GenerateUUID(t)
duplicateRoute := testsutil.GenerateUUID(t)
duplicateDomain := testsutil.GenerateUUID(t)
duplicateChannel := channels.Channel{
ID: testsutil.GenerateUUID(t),
Domain: duplicateDomain,
Name: namegen.Generate(),
Route: duplicateRoute,
}
_, err := repo.Save(context.Background(), duplicateChannel)
require.Nil(t, err, fmt.Sprintf("save channel unexpected error: %s", err))
cases := []struct {
desc string
channel channels.Channel
@@ -149,6 +163,17 @@ func TestSave(t *testing.T) {
},
err: nil,
},
{
desc: "add channel with duplicate route",
channel: channels.Channel{
ID: testsutil.GenerateUUID(t),
Domain: duplicateDomain,
Name: namegen.Generate(),
Route: duplicateRoute,
},
resp: []channels.Channel{},
err: repoerr.ErrConflict,
},
}
for _, tc := range cases {
@@ -183,6 +208,7 @@ func TestUpdate(t *testing.T) {
channel: channels.Channel{
ID: validChannel.ID,
Name: namegen.Generate(),
Route: testsutil.GenerateUUID(t),
Metadata: map[string]interface{}{"key": "value"},
UpdatedAt: validTimestamp,
UpdatedBy: testsutil.GenerateUUID(t),
@@ -330,6 +356,7 @@ func TestChangeStatus(t *testing.T) {
disabledChannel := validChannel
disabledChannel.ID = testsutil.GenerateUUID(t)
disabledChannel.Name = namegen.Generate()
disabledChannel.Route = testsutil.GenerateUUID(t)
disabledChannel.Status = channels.DisabledStatus
_, err := repo.Save(context.Background(), validChannel, disabledChannel)
@@ -442,6 +469,69 @@ func TestRetrieveByID(t *testing.T) {
}
}
func TestRetrieveByRoute(t *testing.T) {
t.Cleanup(func() {
_, err := db.Exec("DELETE FROM channels")
require.Nil(t, err, fmt.Sprintf("clean channels unexpected error: %s", err))
})
repo := postgres.NewRepository(database)
_, err := repo.Save(context.Background(), validChannel)
require.Nil(t, err, fmt.Sprintf("save channel unexpected error: %s", err))
cases := []struct {
desc string
route string
domainID string
resp channels.Channel
err error
}{
{
desc: "retrieve channel by route successfully",
route: validChannel.Route,
domainID: validChannel.Domain,
resp: validChannel,
err: nil,
},
{
desc: "retrieve channel by id with invalid route",
route: "invalid-route",
domainID: validChannel.Domain,
err: repoerr.ErrNotFound,
},
{
desc: "retrieve channel by id with empty route",
route: "",
domainID: validChannel.Domain,
err: repoerr.ErrNotFound,
},
{
desc: "retrieve channel by id with invalid domain",
route: validChannel.Route,
domainID: "invalid-domain",
err: repoerr.ErrNotFound,
},
{
desc: "retrieve channel by id with empty domain",
route: validChannel.Route,
domainID: "",
err: repoerr.ErrNotFound,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
channel, err := repo.RetrieveByRoute(context.Background(), tc.route, tc.domainID)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Nil(t, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.resp, channel, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.resp, channel))
}
})
}
}
func TestRetrieveAll(t *testing.T) {
t.Cleanup(func() {
_, err := db.Exec("DELETE FROM channels")
@@ -460,6 +550,7 @@ func TestRetrieveAll(t *testing.T) {
Domain: testsutil.GenerateUUID(t),
ParentGroup: parentID,
Name: name,
Route: testsutil.GenerateUUID(t),
Metadata: map[string]interface{}{"name": name},
CreatedAt: time.Now().UTC().Truncate(time.Microsecond),
Status: channels.EnabledStatus,
+11
View File
@@ -62,6 +62,17 @@ func Migration() (*migrate.MemoryMigrationSource, error) {
`ALTER TABLE channels ADD CONSTRAINT channels_domain_id_name_key UNIQUE (domain_id, name)`,
},
},
{
Id: "channels_03",
Up: []string{
`ALTER TABLE channels ADD COLUMN route VARCHAR(36);`,
`CREATE UNIQUE INDEX unique_domain_route_not_null ON channels (domain_id, route) WHERE route IS NOT NULL;`,
},
Down: []string{
`DROP INDEX IF EXISTS unique_domain_route_not_null;`,
`ALTER TABLE channels DROP COLUMN route;`,
},
},
},
}
channelsMigration.Migrations = append(channelsMigration.Migrations, rolesMigration.Migrations...)
+12 -6
View File
@@ -36,8 +36,9 @@ var (
idProvider = uuid.New()
namegen = namegenerator.NewGenerator()
validChannel = channels.Channel{
ID: testsutil.GenerateUUID(&testing.T{}),
Name: namegen.Generate(),
ID: testsutil.GenerateUUID(&testing.T{}),
Name: namegen.Generate(),
Route: namegen.Generate(),
Metadata: map[string]interface{}{
"key": "value",
},
@@ -46,8 +47,9 @@ var (
Status: channels.EnabledStatus,
}
validChannelWithRoles = channels.Channel{
ID: testsutil.GenerateUUID(&testing.T{}),
Name: namegen.Generate(),
ID: testsutil.GenerateUUID(&testing.T{}),
Name: namegen.Generate(),
Route: namegen.Generate(),
Metadata: map[string]interface{}{
"key": "value",
},
@@ -91,6 +93,9 @@ func newService(t *testing.T) channels.Service {
func TestCreateChannel(t *testing.T) {
svc := newService(t)
etChan := validChannel
etChan.Route = ""
cases := []struct {
desc string
channel channels.Channel
@@ -292,8 +297,9 @@ func TestUpdateChannel(t *testing.T) {
{
desc: "update channel successfully",
channel: channels.Channel{
ID: testsutil.GenerateUUID(t),
Name: namegen.Generate(),
ID: testsutil.GenerateUUID(t),
Name: namegen.Generate(),
Route: namegen.Generate(),
},
repoResp: validChannel,
},
+1
View File
@@ -25,6 +25,7 @@ type Channel struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
Route string `json:"route,omitempty"`
ParentGroup string `json:"parent_group_id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
+12 -2
View File
@@ -54,12 +54,14 @@ func TestCreateChannel(t *testing.T) {
createChannelReq := channels.Channel{
Name: channel.Name,
Route: channel.Route,
Metadata: channels.Metadata{"role": "client"},
Status: channels.EnabledStatus,
}
channelReq := sdk.Channel{
Name: channel.Name,
Route: channel.Route,
Metadata: validMetadata,
Status: channels.EnabledStatus.String(),
}
@@ -134,6 +136,7 @@ func TestCreateChannel(t *testing.T) {
desc: "create channel with parent group",
channelReq: sdk.Channel{
Name: channel.Name,
Route: channel.Route,
ParentGroup: parentID,
Status: channels.EnabledStatus.String(),
},
@@ -142,6 +145,7 @@ func TestCreateChannel(t *testing.T) {
createChannelReq: channels.Channel{
Name: channel.Name,
ParentGroup: parentID,
Route: channel.Route,
Status: channels.EnabledStatus,
},
svcRes: []channels.Channel{convertChannel(pChannel)},
@@ -153,6 +157,7 @@ func TestCreateChannel(t *testing.T) {
desc: "create channel with invalid parent",
channelReq: sdk.Channel{
Name: channel.Name,
Route: channel.Route,
ParentGroup: wrongID,
Status: channels.EnabledStatus.String(),
},
@@ -161,6 +166,7 @@ func TestCreateChannel(t *testing.T) {
createChannelReq: channels.Channel{
Name: channel.Name,
ParentGroup: wrongID,
Route: channel.Route,
Status: channels.EnabledStatus,
},
svcRes: []channels.Channel{},
@@ -173,6 +179,7 @@ func TestCreateChannel(t *testing.T) {
channelReq: sdk.Channel{
ID: channel.ID,
ParentGroup: parentID,
Route: channel.Route,
Name: channel.Name,
Metadata: validMetadata,
CreatedAt: channel.CreatedAt,
@@ -184,6 +191,7 @@ func TestCreateChannel(t *testing.T) {
createChannelReq: channels.Channel{
ID: channel.ID,
ParentGroup: parentID,
Route: channel.Route,
Name: channel.Name,
Metadata: channels.Metadata{"role": "client"},
CreatedAt: channel.CreatedAt,
@@ -294,8 +302,9 @@ func TestCreateChannels(t *testing.T) {
token: validToken,
channelsReq: []sdk.Channel{
{
ID: generateUUID(t),
Name: "channel_1",
ID: generateUUID(t),
Name: "channel_1",
Route: valid,
Metadata: map[string]interface{}{
"test": make(chan int),
},
@@ -2117,6 +2126,7 @@ func generateTestChannel(t *testing.T) sdk.Channel {
ID: testsutil.GenerateUUID(&testing.T{}),
DomainID: testsutil.GenerateUUID(&testing.T{}),
Name: channelName,
Route: valid,
Metadata: sdk.Metadata{"role": "client"},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
+1
View File
@@ -213,6 +213,7 @@ func convertChannel(g sdk.Channel) mgchannels.Channel {
Name: g.Name,
Tags: g.Tags,
ParentGroup: g.ParentGroup,
Route: g.Route,
Domain: g.DomainID,
Metadata: channels.Metadata(g.Metadata),
CreatedAt: g.CreatedAt,