mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
NOISSUE - Reject invitations (#2379)
Signed-off-by: Sammy Oina <sammyoina@gmail.com>
This commit is contained in:
committed by
GitHub
parent
1ce5952d1a
commit
fdaecf54e5
@@ -104,7 +104,7 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/AcceptInvitationReq"
|
||||
responses:
|
||||
"200":
|
||||
"204":
|
||||
description: Invitation accepted.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
@@ -115,6 +115,30 @@ paths:
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/invitations/reject:
|
||||
post:
|
||||
operationId: rejectInvitation
|
||||
summary: Reject invitation
|
||||
description: |
|
||||
Current logged in user rejects invitation to join domain.
|
||||
tags:
|
||||
- Invitations
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/AcceptInvitationReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Invitation rejected.
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/invitations/{user_id}/{domain_id}:
|
||||
get:
|
||||
operationId: getInvitation
|
||||
|
||||
@@ -45,7 +45,7 @@ func main() {
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf(fmt.Sprintf("failed to load %s configuration : %s", svcName, err))
|
||||
log.Fatalf("failed to load %s configuration : %s", svcName, err)
|
||||
}
|
||||
|
||||
logger, err := mglog.New(os.Stdout, cfg.Server.LogLevel)
|
||||
|
||||
@@ -90,6 +90,21 @@ func acceptInvitationEndpoint(svc invitations.Service) endpoint.Endpoint {
|
||||
}
|
||||
}
|
||||
|
||||
func rejectInvitationEndpoint(svc invitations.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(acceptInvitationReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
if err := svc.RejectInvitation(ctx, req.token, req.DomainID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rejectInvitationRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteInvitationEndpoint(svc invitations.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(invitationReq)
|
||||
|
||||
@@ -488,7 +488,7 @@ func TestAcceptInvitation(t *testing.T) {
|
||||
desc: "valid request",
|
||||
token: validToken,
|
||||
data: fmt.Sprintf(`{"domain_id": "%s"}`, validID),
|
||||
status: http.StatusOK,
|
||||
status: http.StatusNoContent,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
@@ -543,3 +543,74 @@ func TestAcceptInvitation(t *testing.T) {
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectInvitation(t *testing.T) {
|
||||
is, svc := newIvitationsServer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
data string
|
||||
contentType string
|
||||
status int
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: validToken,
|
||||
data: fmt.Sprintf(`{"domain_id": "%s"}`, validID),
|
||||
status: http.StatusNoContent,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid token",
|
||||
token: "",
|
||||
data: fmt.Sprintf(`{"domain_id": "%s"}`, validID),
|
||||
status: http.StatusUnauthorized,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "unauthorized error",
|
||||
token: validToken,
|
||||
data: fmt.Sprintf(`{"domain_id": "%s"}`, "invalid"),
|
||||
status: http.StatusForbidden,
|
||||
contentType: validContenType,
|
||||
svcErr: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "invalid content type",
|
||||
token: validToken,
|
||||
data: fmt.Sprintf(`{"domain_id": "%s"}`, validID),
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
contentType: "text/plain",
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid data",
|
||||
token: validToken,
|
||||
data: `data`,
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
repoCall := svc.On("RejectInvitation", mock.Anything, tc.token, mock.Anything).Return(tc.svcErr)
|
||||
req := testRequest{
|
||||
client: is.Client(),
|
||||
method: http.MethodPost,
|
||||
url: is.URL + "/invitations/reject",
|
||||
token: tc.token,
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.data),
|
||||
}
|
||||
|
||||
res, err := req.make()
|
||||
assert.Nil(t, err, tc.desc)
|
||||
assert.Equal(t, tc.status, res.StatusCode, tc.desc)
|
||||
repoCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ var (
|
||||
_ magistrala.Response = (*viewInvitationRes)(nil)
|
||||
_ magistrala.Response = (*listInvitationsRes)(nil)
|
||||
_ magistrala.Response = (*acceptInvitationRes)(nil)
|
||||
_ magistrala.Response = (*rejectInvitationRes)(nil)
|
||||
_ magistrala.Response = (*deleteInvitationRes)(nil)
|
||||
)
|
||||
|
||||
@@ -69,7 +70,7 @@ func (res listInvitationsRes) Empty() bool {
|
||||
type acceptInvitationRes struct{}
|
||||
|
||||
func (res acceptInvitationRes) Code() int {
|
||||
return http.StatusOK
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res acceptInvitationRes) Headers() map[string]string {
|
||||
@@ -93,3 +94,17 @@ func (res deleteInvitationRes) Headers() map[string]string {
|
||||
func (res deleteInvitationRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type rejectInvitationRes struct{}
|
||||
|
||||
func (res rejectInvitationRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res rejectInvitationRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res rejectInvitationRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ func MakeHandler(svc invitations.Service, logger *slog.Logger, instanceID string
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "accept_invitation").ServeHTTP)
|
||||
r.Post("/reject", otelhttp.NewHandler(kithttp.NewServer(
|
||||
rejectInvitationEndpoint(svc),
|
||||
decodeAcceptInvitationReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "reject_invitation").ServeHTTP)
|
||||
})
|
||||
|
||||
mux.Get("/health", magistrala.Health("invitations", instanceID))
|
||||
|
||||
@@ -22,6 +22,7 @@ type Invitation struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
ConfirmedAt time.Time `json:"confirmed_at,omitempty"`
|
||||
RejectedAt time.Time `json:"rejected_at,omitempty"`
|
||||
Resend bool `json:"resend,omitempty"`
|
||||
}
|
||||
|
||||
@@ -93,6 +94,11 @@ type Service interface {
|
||||
// - domain administrators
|
||||
// - platform administrators
|
||||
DeleteInvitation(ctx context.Context, token, userID, domainID string) (err error)
|
||||
|
||||
// RejectInvitation rejects an invitation.
|
||||
// People who can reject invitations are:
|
||||
// - the invited user: they can reject their own invitations
|
||||
RejectInvitation(ctx context.Context, token, domainID string) (err error)
|
||||
}
|
||||
|
||||
//go:generate mockery --name Repository --output=./mocks --filename repository.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
@@ -112,6 +118,9 @@ type Repository interface {
|
||||
// UpdateConfirmation updates an invitation by setting the confirmation time.
|
||||
UpdateConfirmation(ctx context.Context, invitation Invitation) (err error)
|
||||
|
||||
// UpdateRejection updates an invitation by setting the rejection time.
|
||||
UpdateRejection(ctx context.Context, invitation Invitation) (err error)
|
||||
|
||||
// Delete deletes an invitation.
|
||||
Delete(ctx context.Context, userID, domainID string) (err error)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestInvitation_MarshalJSON(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
res: `{"total":1,"offset":0,"limit":0,"invitations":[{"invited_by":"John","user_id":"123","domain_id":"123","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","confirmed_at":"0001-01-01T00:00:00Z"}]}`,
|
||||
res: `{"total":1,"offset":0,"limit":0,"invitations":[{"invited_by":"John","user_id":"123","domain_id":"123","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","confirmed_at":"0001-01-01T00:00:00Z","rejected_at":"0001-01-01T00:00:00Z"}]}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,22 @@ func (lm *logging) AcceptInvitation(ctx context.Context, token, domainID string)
|
||||
return lm.svc.AcceptInvitation(ctx, token, domainID)
|
||||
}
|
||||
|
||||
func (lm *logging) RejectInvitation(ctx context.Context, token, domainID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("domain_id", domainID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Reject invitation failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Reject invitation completed successfully", args...)
|
||||
}(time.Now())
|
||||
return lm.svc.RejectInvitation(ctx, token, domainID)
|
||||
}
|
||||
|
||||
func (lm *logging) DeleteInvitation(ctx context.Context, token, userID, domainID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
|
||||
@@ -59,6 +59,14 @@ func (mm *metricsmw) AcceptInvitation(ctx context.Context, token, domainID strin
|
||||
return mm.svc.AcceptInvitation(ctx, token, domainID)
|
||||
}
|
||||
|
||||
func (mm *metricsmw) RejectInvitation(ctx context.Context, token, domainID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "reject_invitation").Add(1)
|
||||
mm.latency.With("method", "reject_invitation").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
return mm.svc.RejectInvitation(ctx, token, domainID)
|
||||
}
|
||||
|
||||
func (mm *metricsmw) DeleteInvitation(ctx context.Context, token, userID, domainID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "delete_invitation").Add(1)
|
||||
|
||||
@@ -64,6 +64,15 @@ func (tm *tracing) AcceptInvitation(ctx context.Context, token, domainID string)
|
||||
return tm.svc.AcceptInvitation(ctx, token, domainID)
|
||||
}
|
||||
|
||||
func (tm *tracing) RejectInvitation(ctx context.Context, token, domainID string) (err error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "reject_invitation", trace.WithAttributes(
|
||||
attribute.String("domain_id", domainID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RejectInvitation(ctx, token, domainID)
|
||||
}
|
||||
|
||||
func (tm *tracing) DeleteInvitation(ctx context.Context, token, userID, domainID string) (err error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "delete_invitation", trace.WithAttributes(
|
||||
attribute.String("user_id", userID),
|
||||
|
||||
@@ -126,6 +126,24 @@ func (_m *Repository) UpdateConfirmation(ctx context.Context, invitation invitat
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateRejection provides a mock function with given fields: ctx, invitation
|
||||
func (_m *Repository) UpdateRejection(ctx context.Context, invitation invitations.Invitation) error {
|
||||
ret := _m.Called(ctx, invitation)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateRejection")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, invitations.Invitation) error); ok {
|
||||
r0 = rf(ctx, invitation)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateToken provides a mock function with given fields: ctx, invitation
|
||||
func (_m *Repository) UpdateToken(ctx context.Context, invitation invitations.Invitation) error {
|
||||
ret := _m.Called(ctx, invitation)
|
||||
|
||||
@@ -80,6 +80,24 @@ func (_m *Service) ListInvitations(ctx context.Context, token string, page invit
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RejectInvitation provides a mock function with given fields: ctx, token, domainID
|
||||
func (_m *Service) RejectInvitation(ctx context.Context, token string, domainID string) error {
|
||||
ret := _m.Called(ctx, token, domainID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RejectInvitation")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, token, domainID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendInvitation provides a mock function with given fields: ctx, token, invitation
|
||||
func (_m *Service) SendInvitation(ctx context.Context, token string, invitation invitations.Invitation) error {
|
||||
ret := _m.Called(ctx, token, invitation)
|
||||
|
||||
@@ -32,6 +32,17 @@ func Migration() *migrate.MemoryMigrationSource {
|
||||
`DROP TABLE IF EXISTS invitations`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "invitations_02_add_rejection",
|
||||
Up: []string{
|
||||
`ALTER TABLE invitations
|
||||
ADD COLUMN rejected_at TIMESTAMP`,
|
||||
},
|
||||
Down: []string{
|
||||
`ALTER TABLE invitations
|
||||
DROP COLUMN rejected_at`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,21 @@ func (repo *repository) UpdateConfirmation(ctx context.Context, invitation invit
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *repository) UpdateRejection(ctx context.Context, invitation invitations.Invitation) (err error) {
|
||||
q := `UPDATE invitations SET rejected_at = :rejected_at, updated_at = :updated_at WHERE user_id = :user_id AND domain_id = :domain_id`
|
||||
|
||||
dbInv := toDBInvitation(invitation)
|
||||
result, err := repo.db.NamedExecContext(ctx, q, dbInv)
|
||||
if err != nil {
|
||||
return postgres.HandleError(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *repository) Delete(ctx context.Context, userID, domain string) (err error) {
|
||||
q := `DELETE FROM invitations WHERE user_id = $1 AND domain_id = $2`
|
||||
|
||||
@@ -165,6 +180,9 @@ func pageQuery(pm invitations.Page) string {
|
||||
if pm.State == invitations.Pending {
|
||||
query = append(query, "confirmed_at IS NULL")
|
||||
}
|
||||
if pm.State == invitations.Rejected {
|
||||
query = append(query, "rejected_at IS NOT NULL")
|
||||
}
|
||||
|
||||
if len(query) > 0 {
|
||||
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
|
||||
@@ -182,17 +200,20 @@ type dbInvitation struct {
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
ConfirmedAt sql.NullTime `db:"confirmed_at,omitempty"`
|
||||
RejectedAt sql.NullTime `db:"rejected_at,omitempty"`
|
||||
}
|
||||
|
||||
func toDBInvitation(inv invitations.Invitation) dbInvitation {
|
||||
var updatedAt sql.NullTime
|
||||
var updatedAt, confirmedAt, rejectedAt sql.NullTime
|
||||
if inv.UpdatedAt != (time.Time{}) {
|
||||
updatedAt = sql.NullTime{Time: inv.UpdatedAt, Valid: true}
|
||||
}
|
||||
var confirmedAt sql.NullTime
|
||||
if inv.ConfirmedAt != (time.Time{}) {
|
||||
confirmedAt = sql.NullTime{Time: inv.ConfirmedAt, Valid: true}
|
||||
}
|
||||
if inv.RejectedAt != (time.Time{}) {
|
||||
rejectedAt = sql.NullTime{Time: inv.RejectedAt, Valid: true}
|
||||
}
|
||||
|
||||
return dbInvitation{
|
||||
InvitedBy: inv.InvitedBy,
|
||||
@@ -203,18 +224,21 @@ func toDBInvitation(inv invitations.Invitation) dbInvitation {
|
||||
CreatedAt: inv.CreatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
ConfirmedAt: confirmedAt,
|
||||
RejectedAt: rejectedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toInvitation(dbinv dbInvitation) invitations.Invitation {
|
||||
var updatedAt time.Time
|
||||
var updatedAt, confirmedAt, rejectedAt time.Time
|
||||
if dbinv.UpdatedAt.Valid {
|
||||
updatedAt = dbinv.UpdatedAt.Time
|
||||
}
|
||||
var confirmedAt time.Time
|
||||
if dbinv.ConfirmedAt.Valid {
|
||||
confirmedAt = dbinv.ConfirmedAt.Time
|
||||
}
|
||||
if dbinv.RejectedAt.Valid {
|
||||
rejectedAt = dbinv.RejectedAt.Time
|
||||
}
|
||||
|
||||
return invitations.Invitation{
|
||||
InvitedBy: dbinv.InvitedBy,
|
||||
@@ -225,5 +249,6 @@ func toInvitation(dbinv dbInvitation) invitations.Invitation {
|
||||
CreatedAt: dbinv.CreatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
ConfirmedAt: confirmedAt,
|
||||
RejectedAt: rejectedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
ipostgres "github.com/absmach/magistrala/invitations/postgres"
|
||||
"github.com/absmach/magistrala/pkg/postgres"
|
||||
pgClient "github.com/absmach/magistrala/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
@@ -64,7 +63,7 @@ func TestMain(m *testing.M) {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
dbConfig := pgClient.Config{
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
@@ -76,11 +75,11 @@ func TestMain(m *testing.M) {
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = pgClient.Setup(dbConfig, *ipostgres.Migration()); err != nil {
|
||||
if db, err = postgres.Setup(dbConfig, *ipostgres.Migration()); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
if db, err = pgClient.Connect(dbConfig); err != nil {
|
||||
if db, err = postgres.Connect(dbConfig); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
database = postgres.NewDatabase(db, dbConfig, tracer)
|
||||
|
||||
+49
-15
@@ -130,23 +130,57 @@ func (svc *service) AcceptInvitation(ctx context.Context, token, domainID string
|
||||
return err
|
||||
}
|
||||
|
||||
if inv.UserID == user.GetUserId() && inv.ConfirmedAt.IsZero() {
|
||||
req := mgsdk.UsersRelationRequest{
|
||||
Relation: inv.Relation,
|
||||
UserIDs: []string{user.GetUserId()},
|
||||
}
|
||||
if sdkerr := svc.sdk.AddUserToDomain(inv.DomainID, req, inv.Token); sdkerr != nil {
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
inv.ConfirmedAt = time.Now()
|
||||
inv.UpdatedAt = time.Now()
|
||||
if err := svc.repo.UpdateConfirmation(ctx, inv); err != nil {
|
||||
return err
|
||||
}
|
||||
if inv.UserID != user.GetUserId() {
|
||||
return svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
return nil
|
||||
if !inv.ConfirmedAt.IsZero() {
|
||||
return svcerr.ErrInvitationAlreadyAccepted
|
||||
}
|
||||
|
||||
if !inv.RejectedAt.IsZero() {
|
||||
return svcerr.ErrInvitationAlreadyRejected
|
||||
}
|
||||
|
||||
req := mgsdk.UsersRelationRequest{
|
||||
Relation: inv.Relation,
|
||||
UserIDs: []string{user.GetUserId()},
|
||||
}
|
||||
if sdkerr := svc.sdk.AddUserToDomain(inv.DomainID, req, inv.Token); sdkerr != nil {
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
inv.ConfirmedAt = time.Now()
|
||||
inv.UpdatedAt = inv.ConfirmedAt
|
||||
return svc.repo.UpdateConfirmation(ctx, inv)
|
||||
}
|
||||
|
||||
func (svc *service) RejectInvitation(ctx context.Context, token, domainID string) error {
|
||||
user, err := svc.identify(ctx, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
inv, err := svc.repo.Retrieve(ctx, user.GetUserId(), domainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inv.UserID != user.GetUserId() {
|
||||
return svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if !inv.ConfirmedAt.IsZero() {
|
||||
return svcerr.ErrInvitationAlreadyAccepted
|
||||
}
|
||||
|
||||
if !inv.RejectedAt.IsZero() {
|
||||
return svcerr.ErrInvitationAlreadyRejected
|
||||
}
|
||||
|
||||
inv.RejectedAt = time.Now()
|
||||
inv.UpdatedAt = inv.RejectedAt
|
||||
return svc.repo.UpdateRejection(ctx, inv)
|
||||
}
|
||||
|
||||
func (svc *service) DeleteInvitation(ctx context.Context, token, userID, domainID string) error {
|
||||
|
||||
+153
-10
@@ -5,7 +5,6 @@ package invitations_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
"github.com/absmach/magistrala/pkg/apiutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
sdkmocks "github.com/absmach/magistrala/pkg/sdk/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -577,8 +577,8 @@ func TestListInvitations(t *testing.T) {
|
||||
func TestAcceptInvitation(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
authsvc := new(authmocks.AuthServiceClient)
|
||||
svc := invitations.NewService(repo, authsvc, nil)
|
||||
|
||||
sdksvc := new(sdkmocks.SDK)
|
||||
svc := invitations.NewService(repo, authsvc, sdksvc)
|
||||
userID := testsutil.GenerateUUID(t)
|
||||
|
||||
cases := []struct {
|
||||
@@ -593,6 +593,8 @@ func TestAcceptInvitation(t *testing.T) {
|
||||
adminErr error
|
||||
authorised bool
|
||||
repoErr error
|
||||
sdkErr errors.SDKError
|
||||
repoErr1 error
|
||||
}{
|
||||
{
|
||||
desc: "invalid token",
|
||||
@@ -606,16 +608,15 @@ func TestAcceptInvitation(t *testing.T) {
|
||||
repoErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "list invitations successful that have been confirmed",
|
||||
desc: "accept invitation successful",
|
||||
token: validToken,
|
||||
tokenUserID: userID,
|
||||
domainID: "",
|
||||
resp: invitations.Invitation{
|
||||
UserID: userID,
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
Token: validToken,
|
||||
Relation: auth.ContributorRelation,
|
||||
ConfirmedAt: time.Now().Add(-time.Second * time.Duration(rand.Intn(100))),
|
||||
UserID: userID,
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
Token: validToken,
|
||||
Relation: auth.ContributorRelation,
|
||||
},
|
||||
err: nil,
|
||||
authNErr: nil,
|
||||
@@ -625,7 +626,7 @@ func TestAcceptInvitation(t *testing.T) {
|
||||
repoErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "list invitations with failed to retrieve all",
|
||||
desc: "accept invitation with failed to retrieve all",
|
||||
token: validToken,
|
||||
tokenUserID: userID,
|
||||
err: svcerr.ErrNotFound,
|
||||
@@ -635,15 +636,57 @@ func TestAcceptInvitation(t *testing.T) {
|
||||
authorised: false,
|
||||
repoErr: svcerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "accept invitation with sdk err",
|
||||
token: validToken,
|
||||
tokenUserID: userID,
|
||||
domainID: "",
|
||||
resp: invitations.Invitation{
|
||||
UserID: userID,
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
Token: validToken,
|
||||
Relation: auth.ContributorRelation,
|
||||
},
|
||||
err: errors.NewSDKError(svcerr.ErrConflict),
|
||||
authNErr: nil,
|
||||
domainErr: nil,
|
||||
adminErr: nil,
|
||||
authorised: true,
|
||||
repoErr: nil,
|
||||
sdkErr: errors.NewSDKError(svcerr.ErrConflict),
|
||||
},
|
||||
{
|
||||
desc: "accept invitation with failed update confirmation",
|
||||
token: validToken,
|
||||
tokenUserID: userID,
|
||||
domainID: "",
|
||||
resp: invitations.Invitation{
|
||||
UserID: userID,
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
Token: validToken,
|
||||
Relation: auth.ContributorRelation,
|
||||
},
|
||||
err: svcerr.ErrUpdateEntity,
|
||||
authNErr: nil,
|
||||
domainErr: nil,
|
||||
adminErr: nil,
|
||||
authorised: true,
|
||||
repoErr: nil,
|
||||
repoErr1: svcerr.ErrUpdateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(&magistrala.IdentityRes{UserId: tc.tokenUserID}, tc.authNErr)
|
||||
repocall1 := repo.On("Retrieve", context.Background(), mock.Anything, tc.domainID).Return(tc.resp, tc.repoErr)
|
||||
sdkcall := sdksvc.On("AddUserToDomain", mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
|
||||
repocall2 := repo.On("UpdateConfirmation", context.Background(), mock.Anything).Return(tc.repoErr1)
|
||||
err := svc.AcceptInvitation(context.Background(), tc.token, tc.domainID)
|
||||
assert.Equal(t, tc.err, err, tc.desc)
|
||||
repocall.Unset()
|
||||
repocall1.Unset()
|
||||
sdkcall.Unset()
|
||||
repocall2.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,3 +843,103 @@ func TestDeleteInvitation(t *testing.T) {
|
||||
repocall2.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectInvitation(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
authsvc := new(authmocks.AuthServiceClient)
|
||||
svc := invitations.NewService(repo, authsvc, nil)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
tokenUserID string
|
||||
userID string
|
||||
domainID string
|
||||
resp invitations.Invitation
|
||||
err error
|
||||
authNErr error
|
||||
authorised bool
|
||||
repoErr error
|
||||
repoErr1 error
|
||||
}{
|
||||
{
|
||||
desc: "invalid token",
|
||||
token: "invalid",
|
||||
tokenUserID: "",
|
||||
userID: testsutil.GenerateUUID(t),
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
err: svcerr.ErrAuthentication,
|
||||
authNErr: svcerr.ErrAuthentication,
|
||||
authorised: false,
|
||||
repoErr: nil,
|
||||
repoErr1: nil,
|
||||
},
|
||||
{
|
||||
desc: "reject invitations for the same user",
|
||||
token: validToken,
|
||||
tokenUserID: validInvitation.UserID,
|
||||
userID: validInvitation.UserID,
|
||||
domainID: validInvitation.DomainID,
|
||||
resp: validInvitation,
|
||||
err: nil,
|
||||
authNErr: nil,
|
||||
authorised: true,
|
||||
repoErr: nil,
|
||||
repoErr1: nil,
|
||||
},
|
||||
{
|
||||
desc: "reject invitations for the invited user",
|
||||
token: validToken,
|
||||
tokenUserID: validInvitation.InvitedBy,
|
||||
userID: validInvitation.UserID,
|
||||
domainID: validInvitation.DomainID,
|
||||
resp: validInvitation,
|
||||
err: svcerr.ErrAuthorization,
|
||||
authNErr: nil,
|
||||
authorised: true,
|
||||
repoErr: nil,
|
||||
repoErr1: nil,
|
||||
},
|
||||
{
|
||||
desc: "error retrieving invitation",
|
||||
token: validToken,
|
||||
tokenUserID: testsutil.GenerateUUID(t),
|
||||
userID: validInvitation.UserID,
|
||||
domainID: validInvitation.DomainID,
|
||||
resp: invitations.Invitation{},
|
||||
err: svcerr.ErrNotFound,
|
||||
authNErr: nil,
|
||||
authorised: true,
|
||||
repoErr: svcerr.ErrNotFound,
|
||||
repoErr1: nil,
|
||||
},
|
||||
{
|
||||
desc: "error updating rejection",
|
||||
token: validToken,
|
||||
tokenUserID: validInvitation.UserID,
|
||||
userID: validInvitation.UserID,
|
||||
domainID: validInvitation.DomainID,
|
||||
resp: validInvitation,
|
||||
err: svcerr.ErrUpdateEntity,
|
||||
authNErr: nil,
|
||||
authorised: true,
|
||||
repoErr: nil,
|
||||
repoErr1: svcerr.ErrUpdateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
idRes := &magistrala.IdentityRes{
|
||||
UserId: tc.tokenUserID,
|
||||
Id: tc.domainID + "_" + tc.userID,
|
||||
}
|
||||
repocall := authsvc.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(idRes, tc.authNErr)
|
||||
repocall1 := repo.On("Retrieve", context.Background(), mock.Anything, mock.Anything).Return(tc.resp, tc.repoErr)
|
||||
repocall3 := repo.On("UpdateRejection", context.Background(), mock.Anything).Return(tc.repoErr1)
|
||||
err := svc.RejectInvitation(context.Background(), tc.token, tc.domainID)
|
||||
assert.Equal(t, tc.err, err, tc.desc)
|
||||
repocall.Unset()
|
||||
repocall1.Unset()
|
||||
repocall3.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
All State = iota // All is used for querying purposes to list invitations irrespective of their state - both pending and accepted.
|
||||
Pending // Pending is the state of an invitation that has not been accepted yet.
|
||||
Accepted // Accepted is the state of an invitation that has been accepted.
|
||||
Rejected // Rejected is the state of an invitation that has been rejected.
|
||||
)
|
||||
|
||||
// String representation of the possible state values.
|
||||
@@ -24,6 +25,7 @@ const (
|
||||
all = "all"
|
||||
pending = "pending"
|
||||
accepted = "accepted"
|
||||
rejected = "rejected"
|
||||
unknown = "unknown"
|
||||
)
|
||||
|
||||
@@ -36,6 +38,8 @@ func (s State) String() string {
|
||||
return pending
|
||||
case Accepted:
|
||||
return accepted
|
||||
case Rejected:
|
||||
return rejected
|
||||
default:
|
||||
return unknown
|
||||
}
|
||||
@@ -50,6 +54,8 @@ func ToState(status string) (State, error) {
|
||||
return Pending, nil
|
||||
case accepted:
|
||||
return Accepted, nil
|
||||
case rejected:
|
||||
return Rejected, nil
|
||||
}
|
||||
|
||||
return State(0), apiutil.ErrInvitationState
|
||||
|
||||
@@ -19,6 +19,7 @@ func TestState_String(t *testing.T) {
|
||||
}{
|
||||
{"Pending", invitations.Pending, "pending"},
|
||||
{"Accepted", invitations.Accepted, "accepted"},
|
||||
{"Rejected", invitations.Rejected, "rejected"},
|
||||
{"All", invitations.All, "all"},
|
||||
{"Unknown", invitations.State(100), "unknown"},
|
||||
}
|
||||
@@ -38,6 +39,7 @@ func TestToState(t *testing.T) {
|
||||
}{
|
||||
{"Pending", "pending", invitations.Pending, nil},
|
||||
{"Accepted", "accepted", invitations.Accepted, nil},
|
||||
{"Rejected", "rejected", invitations.Rejected, nil},
|
||||
{"All", "all", invitations.All, nil},
|
||||
{"Unknown", "unknown", invitations.State(0), apiutil.ErrInvitationState},
|
||||
}
|
||||
@@ -58,6 +60,7 @@ func TestState_MarshalJSON(t *testing.T) {
|
||||
}{
|
||||
{"Pending", invitations.Pending, []byte(`"pending"`), nil},
|
||||
{"Accepted", invitations.Accepted, []byte(`"accepted"`), nil},
|
||||
{"Rejected", invitations.Rejected, []byte(`"rejected"`), nil},
|
||||
{"All", invitations.All, []byte(`"all"`), nil},
|
||||
{"Unknown", invitations.State(100), []byte(`"unknown"`), nil},
|
||||
}
|
||||
@@ -78,6 +81,7 @@ func TestState_UnmarshalJSON(t *testing.T) {
|
||||
}{
|
||||
{"Pending", []byte(`"pending"`), invitations.Pending, nil},
|
||||
{"Accepted", []byte(`"accepted"`), invitations.Accepted, nil},
|
||||
{"Rejected", []byte(`"rejected"`), invitations.Rejected, nil},
|
||||
{"All", []byte(`"all"`), invitations.All, nil},
|
||||
{"Unknown", []byte(`"unknown"`), invitations.State(0), apiutil.ErrInvitationState},
|
||||
}
|
||||
|
||||
@@ -63,4 +63,10 @@ var (
|
||||
|
||||
// ErrSearch indicates error in searching clients.
|
||||
ErrSearch = errors.New("failed to search clients")
|
||||
|
||||
// ErrInvitationAlreadyRejected indicates that the invitation is already rejected.
|
||||
ErrInvitationAlreadyRejected = errors.New("invitation already rejected")
|
||||
|
||||
// ErrInvitationAlreadyAccepted indicates that the invitation is already accepted.
|
||||
ErrInvitationAlreadyAccepted = errors.New("invitation already accepted")
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
logger, err = mglog.New(os.Stdout, "debug")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
@@ -79,7 +79,7 @@ func TestMain(m *testing.M) {
|
||||
defer func() {
|
||||
err = pubsub.Close()
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
logger, err := mglog.New(os.Stdout, "error")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
if err := pool.Retry(func() error {
|
||||
pubsub, err = nats.NewPubSub(context.Background(), address, logger)
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
logger, err = mglog.New(os.Stdout, "debug")
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
if err := pool.Retry(func() error {
|
||||
pubsub, err = rabbitmq.NewPubSub(address, logger)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
const (
|
||||
invitationsEndpoint = "invitations"
|
||||
acceptEndpoint = "accept"
|
||||
rejectEndpoint = "reject"
|
||||
)
|
||||
|
||||
type Invitation struct {
|
||||
@@ -95,7 +96,25 @@ func (sdk mgSDK) AcceptInvitation(domainID, token string) (err error) {
|
||||
|
||||
url := sdk.invitationsURL + "/" + invitationsEndpoint + "/" + acceptEndpoint
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusOK)
|
||||
_, _, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusNoContent)
|
||||
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) RejectInvitation(domainID, token string) (err error) {
|
||||
req := struct {
|
||||
DomainID string `json:"domain_id"`
|
||||
}{
|
||||
DomainID: domainID,
|
||||
}
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := sdk.invitationsURL + "/" + invitationsEndpoint + "/" + rejectEndpoint
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(http.MethodDelete, url, token, data, nil, http.StatusNoContent)
|
||||
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
@@ -1167,6 +1167,13 @@ type SDK interface {
|
||||
// fmt.Println(err)
|
||||
AcceptInvitation(domainID, token string) (err error)
|
||||
|
||||
// RejectInvitation rejects an invitation.
|
||||
//
|
||||
// For example:
|
||||
// err := sdk.RejectInvitation("domainID", "token")
|
||||
// fmt.Println(err)
|
||||
RejectInvitation(domainID, token string) (err error)
|
||||
|
||||
// DeleteInvitation deletes an invitation.
|
||||
//
|
||||
// For example:
|
||||
|
||||
@@ -1880,6 +1880,24 @@ func (_m *SDK) RefreshToken(lt sdk.Login, token string) (sdk.Token, errors.SDKEr
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RejectInvitation provides a mock function with given fields: domainID, token
|
||||
func (_m *SDK) RejectInvitation(domainID string, token string) error {
|
||||
ret := _m.Called(domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RejectInvitation")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(domainID, token)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveBootstrap provides a mock function with given fields: id, token
|
||||
func (_m *SDK) RemoveBootstrap(id string, token string) errors.SDKError {
|
||||
ret := _m.Called(id, token)
|
||||
|
||||
Reference in New Issue
Block a user