diff --git a/engine/action.go b/engine/action.go index 777abbd91..5e14ddb6e 100644 --- a/engine/action.go +++ b/engine/action.go @@ -79,16 +79,20 @@ type ActionConnCfg struct { } func newActionConnCfg(source, action string, cfg *config.CGRConfig) ActionConnCfg { + sessionActions := []string{ + utils.MetaAlterSessions, + utils.MetaForceDisconnectSessions, + } act := ActionConnCfg{} switch source { case utils.ThresholdS: - switch action { - case utils.MetaAlterSessions: + switch { + case slices.Contains(sessionActions, action): act.ConnIDs = cfg.ThresholdSCfg().SessionSConns } case utils.RALs: - switch action { - case utils.MetaAlterSessions: + switch { + case slices.Contains(sessionActions, action): act.ConnIDs = cfg.RalsCfg().SessionSConns } } @@ -124,6 +128,7 @@ func init() { actionFuncMap[utils.MetaTransferMonetaryDefault] = transferMonetaryDefaultAction actionFuncMap[utils.MetaCgrRpc] = cgrRPCAction actionFuncMap[utils.MetaAlterSessions] = alterSessionsAction + actionFuncMap[utils.MetaForceDisconnectSessions] = forceDisconnectSessionsAction actionFuncMap[utils.TopUpZeroNegative] = topupZeroNegativeAction actionFuncMap[utils.SetExpiry] = setExpiryAction actionFuncMap[utils.MetaPublishAccount] = publishAccount @@ -944,6 +949,50 @@ func alterSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any, return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.SessionSv1AlterSessions, attr, &reply) } +// forceDisconnectSessionsAction processes the `ExtraParameters` field from the action to construct a request +// for the `SessionSv1.ForceDisconnect` API call. +// +// The ExtraParameters field format is expected as follows: +// - tenant: string +// - filters: strings separated by "&". +// - limit: integer, specifying the maximum number of sessions to disconnect. +// - APIOpts: set of key-value pairs (separated by "&"). +// - Event: set of key-value pairs (separated by "&"). +// +// Parameters are separated by ";" and must be provided in the specified order. +func forceDisconnectSessionsAction(_ *Account, act *Action, _ Actions, _ *FilterS, _ any, connCfg ActionConnCfg) (err error) { + + // Parse action parameters based on the predefined format. + params := strings.Split(act.ExtraParameters, ";") + if len(params) != 5 { + return errors.New("invalid number of parameters; expected 5") + } + + // If conversion fails, limit will default to 0. + limit, _ := strconv.Atoi(params[2]) + + // Prepare request arguments based on provided parameters. + attr := utils.SessionFilterWithEvent{ + SessionFilter: &utils.SessionFilter{ + Limit: &limit, + Tenant: params[0], + Filters: strings.Split(params[1], "&"), + APIOpts: make(map[string]any), + }, + Event: make(map[string]any), + } + + if err := parseParamStringToMap(params[3], attr.APIOpts); err != nil { + return err + } + if err := parseParamStringToMap(params[4], attr.Event); err != nil { + return err + } + + var reply string + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.SessionSv1ForceDisconnect, attr, &reply) +} + // parseParamStringToMap parses a string containing key-value pairs separated by "&" and assigns // these pairs to a given map. Each pair is expected to be in the format "key:value". func parseParamStringToMap(paramStr string, targetMap map[string]any) error { diff --git a/engine/actions_test.go b/engine/actions_test.go index 35e491149..5f4a50985 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -4576,21 +4576,17 @@ func TestActionsTransferBalance(t *testing.T) { } } -type mockSessionSv1Obj struct { - request string -} - -func (m *mockSessionSv1Obj) AlterSessions(_ *context.Context, params utils.SessionFilterWithEvent, reply *string) error { - m.request = utils.ToJSON(params) - return nil -} - -func TestActionsAlterSessions(t *testing.T) { - var receivedRequest string +func TestActionsAlterAndDisconnectSessions(t *testing.T) { + var alterSessionsRequest string + var disconnectSessionsRequest string ccMock := &ccMock{ calls: map[string]func(ctx *context.Context, args any, reply any) error{ utils.SessionSv1AlterSessions: func(ctx *context.Context, args, reply any) error { - receivedRequest = utils.ToJSON(args) + alterSessionsRequest = utils.ToJSON(args) + return nil + }, + utils.SessionSv1ForceDisconnect: func(ctx *context.Context, args, reply any) error { + disconnectSessionsRequest = utils.ToJSON(args) return nil }, }, @@ -4641,7 +4637,8 @@ func TestActionsAlterSessions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { action := &Action{ExtraParameters: tc.extraParams} t.Cleanup(func() { - receivedRequest = "" + alterSessionsRequest = "" + disconnectSessionsRequest = "" }) err := alterSessionsAction(nil, action, nil, nil, nil, ActionConnCfg{ ConnIDs: tc.connIDs, @@ -4652,8 +4649,20 @@ func TestActionsAlterSessions(t *testing.T) { } } else if err != nil { t.Error(err) - } else if receivedRequest != tc.expectedRequest { - t.Errorf("expected: %v\nreceived: %v", tc.expectedRequest, receivedRequest) + } else if alterSessionsRequest != tc.expectedRequest { + t.Errorf("expected: %v\nreceived: %v", tc.expectedRequest, alterSessionsRequest) + } + err = forceDisconnectSessionsAction(nil, action, nil, nil, nil, ActionConnCfg{ + ConnIDs: tc.connIDs, + }) + if tc.expectedErr != "" { + if err == nil || err.Error() != tc.expectedErr { + t.Errorf("expected error %v, received %v", tc.expectedErr, err) + } + } else if err != nil { + t.Error(err) + } else if disconnectSessionsRequest != tc.expectedRequest { + t.Errorf("expected: %v\nreceived: %v", tc.expectedRequest, disconnectSessionsRequest) } }) } diff --git a/utils/consts.go b/utils/consts.go index 3e8f310e6..666198693 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1061,6 +1061,7 @@ const ( MetaTransferMonetaryDefault = "*transfer_monetary_default" MetaCgrRpc = "*cgr_rpc" MetaAlterSessions = "*alter_sessions" + MetaForceDisconnectSessions = "*force_disconnect_sessions" TopUpZeroNegative = "*topup_zero_negative" SetExpiry = "*set_expiry" MetaPublishAccount = "*publish_account"