github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/sip/sip_test.go (about)

     1  // Copyright 2023 LiveKit, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package sip
    16  
    17  import (
    18  	"fmt"
    19  	"strconv"
    20  	"testing"
    21  
    22  	"github.com/stretchr/testify/require"
    23  
    24  	"github.com/livekit/protocol/livekit"
    25  	"github.com/livekit/protocol/rpc"
    26  )
    27  
    28  const (
    29  	sipNumber1  = "1111 1111"
    30  	sipNumber2  = "2222 2222"
    31  	sipNumber3  = "3333 3333"
    32  	sipTrunkID1 = "aaa"
    33  	sipTrunkID2 = "bbb"
    34  )
    35  
    36  var trunkCases = []struct {
    37  	name    string
    38  	trunks  []*livekit.SIPTrunkInfo
    39  	exp     int
    40  	expErr  bool
    41  	invalid bool
    42  }{
    43  	{
    44  		name:   "empty",
    45  		trunks: nil,
    46  		exp:    -1, // no error; nil result
    47  	},
    48  	{
    49  		name: "one wildcard",
    50  		trunks: []*livekit.SIPTrunkInfo{
    51  			{SipTrunkId: "aaa"},
    52  		},
    53  		exp: 0,
    54  	},
    55  	{
    56  		name: "matching",
    57  		trunks: []*livekit.SIPTrunkInfo{
    58  			{SipTrunkId: "aaa", OutboundNumber: sipNumber2},
    59  		},
    60  		exp: 0,
    61  	},
    62  	{
    63  		name: "matching inbound",
    64  		trunks: []*livekit.SIPTrunkInfo{
    65  			{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1}},
    66  		},
    67  		exp: 0,
    68  	},
    69  	{
    70  		name: "matching regexp",
    71  		trunks: []*livekit.SIPTrunkInfo{
    72  			{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+ \d+$`}},
    73  		},
    74  		exp: 0,
    75  	},
    76  	{
    77  		name: "not matching",
    78  		trunks: []*livekit.SIPTrunkInfo{
    79  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
    80  		},
    81  		exp: -1,
    82  	},
    83  	{
    84  		name: "not matching inbound",
    85  		trunks: []*livekit.SIPTrunkInfo{
    86  			{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}},
    87  		},
    88  		exp: -1,
    89  	},
    90  	{
    91  		name: "not matching regexp",
    92  		trunks: []*livekit.SIPTrunkInfo{
    93  			{SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+$`}},
    94  		},
    95  		exp: -1,
    96  	},
    97  	{
    98  		name: "one match",
    99  		trunks: []*livekit.SIPTrunkInfo{
   100  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
   101  			{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
   102  		},
   103  		exp: 1,
   104  	},
   105  	{
   106  		name: "many matches",
   107  		trunks: []*livekit.SIPTrunkInfo{
   108  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
   109  			{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
   110  			{SipTrunkId: "ccc", OutboundNumber: sipNumber2},
   111  		},
   112  		expErr:  true,
   113  		invalid: true,
   114  	},
   115  	{
   116  		name: "many matches default",
   117  		trunks: []*livekit.SIPTrunkInfo{
   118  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
   119  			{SipTrunkId: "bbb"},
   120  			{SipTrunkId: "ccc", OutboundNumber: sipNumber2},
   121  			{SipTrunkId: "ddd"},
   122  		},
   123  		exp:     2,
   124  		invalid: true, // it can successfully select "ccc", but the overall configuration is invalid
   125  	},
   126  	{
   127  		name: "inbound",
   128  		trunks: []*livekit.SIPTrunkInfo{
   129  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
   130  			{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
   131  			{SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}},
   132  		},
   133  		exp: 1,
   134  	},
   135  	{
   136  		name: "regexp",
   137  		trunks: []*livekit.SIPTrunkInfo{
   138  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
   139  			{SipTrunkId: "bbb", OutboundNumber: sipNumber2},
   140  			{SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+$`}},
   141  		},
   142  		exp: 1,
   143  	},
   144  	{
   145  		name: "multiple defaults",
   146  		trunks: []*livekit.SIPTrunkInfo{
   147  			{SipTrunkId: "aaa", OutboundNumber: sipNumber3},
   148  			{SipTrunkId: "bbb"},
   149  			{SipTrunkId: "ccc"},
   150  		},
   151  		expErr:  true,
   152  		invalid: true,
   153  	},
   154  }
   155  
   156  func TestSIPMatchTrunk(t *testing.T) {
   157  	for _, c := range trunkCases {
   158  		c := c
   159  		t.Run(c.name, func(t *testing.T) {
   160  			got, err := MatchTrunk(c.trunks, sipNumber1, sipNumber2)
   161  			if c.expErr {
   162  				require.Error(t, err)
   163  				require.Nil(t, got)
   164  				t.Log(err)
   165  			} else {
   166  				var exp *livekit.SIPTrunkInfo
   167  				if c.exp >= 0 {
   168  					exp = c.trunks[c.exp]
   169  				}
   170  				require.NoError(t, err)
   171  				require.Equal(t, exp, got)
   172  			}
   173  		})
   174  	}
   175  }
   176  
   177  func TestSIPValidateTrunks(t *testing.T) {
   178  	for _, c := range trunkCases {
   179  		c := c
   180  		t.Run(c.name, func(t *testing.T) {
   181  			for i, r := range c.trunks {
   182  				if r.SipTrunkId == "" {
   183  					r.SipTrunkId = strconv.Itoa(i)
   184  				}
   185  			}
   186  			err := ValidateTrunks(c.trunks)
   187  			if c.invalid {
   188  				require.Error(t, err)
   189  			} else {
   190  				require.NoError(t, err)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func newSIPTrunkDispatch() *livekit.SIPTrunkInfo {
   197  	return &livekit.SIPTrunkInfo{
   198  		SipTrunkId:     sipTrunkID1,
   199  		OutboundNumber: sipNumber2,
   200  	}
   201  }
   202  
   203  func newSIPReqDispatch(pin string, noPin bool) *rpc.EvaluateSIPDispatchRulesRequest {
   204  	return &rpc.EvaluateSIPDispatchRulesRequest{
   205  		CallingNumber: sipNumber1,
   206  		CalledNumber:  sipNumber2,
   207  		Pin:           pin,
   208  		//NoPin: noPin, // TODO
   209  	}
   210  }
   211  
   212  func newDirectDispatch(room, pin string) *livekit.SIPDispatchRule {
   213  	return &livekit.SIPDispatchRule{
   214  		Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{
   215  			DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{
   216  				RoomName: room, Pin: pin,
   217  			},
   218  		},
   219  	}
   220  }
   221  
   222  func newIndividualDispatch(roomPref, pin string) *livekit.SIPDispatchRule {
   223  	return &livekit.SIPDispatchRule{
   224  		Rule: &livekit.SIPDispatchRule_DispatchRuleIndividual{
   225  			DispatchRuleIndividual: &livekit.SIPDispatchRuleIndividual{
   226  				RoomPrefix: roomPref, Pin: pin,
   227  			},
   228  		},
   229  	}
   230  }
   231  
   232  var dispatchCases = []struct {
   233  	name    string
   234  	trunk   *livekit.SIPTrunkInfo
   235  	rules   []*livekit.SIPDispatchRuleInfo
   236  	reqPin  string
   237  	noPin   bool
   238  	exp     int
   239  	expErr  bool
   240  	invalid bool
   241  }{
   242  	// These cases just validate that no rules produce an error.
   243  	{
   244  		name:   "empty",
   245  		trunk:  nil,
   246  		rules:  nil,
   247  		expErr: true,
   248  	},
   249  	{
   250  		name:   "only trunk",
   251  		trunk:  newSIPTrunkDispatch(),
   252  		rules:  nil,
   253  		expErr: true,
   254  	},
   255  	// Default rules should work even if no trunk is defined.
   256  	{
   257  		name:  "one rule/no trunk",
   258  		trunk: nil,
   259  		rules: []*livekit.SIPDispatchRuleInfo{
   260  			{TrunkIds: nil, Rule: newDirectDispatch("sip", "")},
   261  		},
   262  		exp: 0,
   263  	},
   264  	// Default rule should work with a trunk too.
   265  	{
   266  		name:  "one rule/default trunk",
   267  		trunk: newSIPTrunkDispatch(),
   268  		rules: []*livekit.SIPDispatchRuleInfo{
   269  			{TrunkIds: nil, Rule: newDirectDispatch("sip", "")},
   270  		},
   271  		exp: 0,
   272  	},
   273  	// Rule matching the trunk should be selected.
   274  	{
   275  		name:  "one rule/specific trunk",
   276  		trunk: newSIPTrunkDispatch(),
   277  		rules: []*livekit.SIPDispatchRuleInfo{
   278  			{TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip", "")},
   279  		},
   280  		exp: 0,
   281  	},
   282  	// Rule NOT matching the trunk should NOT be selected.
   283  	{
   284  		name:  "one rule/wrong trunk",
   285  		trunk: newSIPTrunkDispatch(),
   286  		rules: []*livekit.SIPDispatchRuleInfo{
   287  			{TrunkIds: []string{"zzz"}, Rule: newDirectDispatch("sip", "")},
   288  		},
   289  		expErr: true,
   290  	},
   291  	// Direct rule with a pin should be selected, even if no pin is provided.
   292  	{
   293  		name:  "direct pin/correct",
   294  		trunk: newSIPTrunkDispatch(),
   295  		rules: []*livekit.SIPDispatchRuleInfo{
   296  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")},
   297  			{TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")},
   298  		},
   299  		reqPin: "123",
   300  		exp:    0,
   301  	},
   302  	// Direct rule with a pin should reject wrong pin.
   303  	{
   304  		name:  "direct pin/wrong",
   305  		trunk: newSIPTrunkDispatch(),
   306  		rules: []*livekit.SIPDispatchRuleInfo{
   307  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")},
   308  			{TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")},
   309  		},
   310  		reqPin: "zzz",
   311  		expErr: true,
   312  	},
   313  	// Multiple direct rules with the same pin should result in an error.
   314  	{
   315  		name:  "direct pin/conflict",
   316  		trunk: newSIPTrunkDispatch(),
   317  		rules: []*livekit.SIPDispatchRuleInfo{
   318  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")},
   319  			{TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")},
   320  		},
   321  		reqPin:  "123",
   322  		expErr:  true,
   323  		invalid: true,
   324  	},
   325  	// Multiple direct rules with the same pin on different trunks are ok.
   326  	{
   327  		name:  "direct pin/no conflict on different trunk",
   328  		trunk: newSIPTrunkDispatch(),
   329  		rules: []*livekit.SIPDispatchRuleInfo{
   330  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")},
   331  			{TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")},
   332  		},
   333  		reqPin: "123",
   334  		exp:    0,
   335  	},
   336  	// Specific direct rules should take priority over default direct rules.
   337  	{
   338  		name:  "direct pin/default and specific",
   339  		trunk: newSIPTrunkDispatch(),
   340  		rules: []*livekit.SIPDispatchRuleInfo{
   341  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
   342  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")},
   343  		},
   344  		reqPin: "123",
   345  		exp:    1,
   346  	},
   347  	// Specific direct rules should take priority over default direct rules. No pin.
   348  	{
   349  		name:  "direct/default and specific",
   350  		trunk: newSIPTrunkDispatch(),
   351  		rules: []*livekit.SIPDispatchRuleInfo{
   352  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
   353  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")},
   354  		},
   355  		exp: 1,
   356  	},
   357  	// Specific direct rules should take priority over default direct rules. One with pin, other without.
   358  	{
   359  		name:  "direct/default and specific/mixed 1",
   360  		trunk: newSIPTrunkDispatch(),
   361  		rules: []*livekit.SIPDispatchRuleInfo{
   362  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
   363  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")},
   364  		},
   365  		exp: 1,
   366  	},
   367  	{
   368  		name:  "direct/default and specific/mixed 2",
   369  		trunk: newSIPTrunkDispatch(),
   370  		rules: []*livekit.SIPDispatchRuleInfo{
   371  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
   372  			{TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")},
   373  		},
   374  		exp: 1,
   375  	},
   376  	// Multiple default direct rules are not allowed.
   377  	{
   378  		name:  "direct/multiple defaults",
   379  		trunk: newSIPTrunkDispatch(),
   380  		rules: []*livekit.SIPDispatchRuleInfo{
   381  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
   382  			{TrunkIds: nil, Rule: newDirectDispatch("sip2", "")},
   383  		},
   384  		expErr:  true,
   385  		invalid: true,
   386  	},
   387  	// Rules for specific numbers take priority.
   388  	{
   389  		name:  "direct/number specific",
   390  		trunk: newSIPTrunkDispatch(),
   391  		rules: []*livekit.SIPDispatchRuleInfo{
   392  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "")},
   393  			{TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}},
   394  		},
   395  		exp: 1,
   396  	},
   397  	{
   398  		name:  "direct/number specific pin",
   399  		trunk: newSIPTrunkDispatch(),
   400  		rules: []*livekit.SIPDispatchRuleInfo{
   401  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
   402  			{TrunkIds: nil, Rule: newDirectDispatch("sip2", "123"), InboundNumbers: []string{sipNumber1}},
   403  		},
   404  		exp: 1,
   405  	},
   406  	{
   407  		name:  "direct/number specific conflict",
   408  		trunk: newSIPTrunkDispatch(),
   409  		rules: []*livekit.SIPDispatchRuleInfo{
   410  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", ""), InboundNumbers: []string{sipNumber1}},
   411  			{TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1, sipNumber2}},
   412  		},
   413  		expErr:  true,
   414  		invalid: true,
   415  	},
   416  	// Check the "personal room" use case. Rule that accepts a number without a pin and requires pin for everyone else.
   417  	{
   418  		name:  "direct/open specific vs pin generic",
   419  		trunk: newSIPTrunkDispatch(),
   420  		rules: []*livekit.SIPDispatchRuleInfo{
   421  			{TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")},
   422  			{TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}},
   423  		},
   424  		exp: 1,
   425  	},
   426  	// Cannot use both direct and individual rules with the same pin setup.
   427  	{
   428  		name:  "direct vs individual/private",
   429  		trunk: newSIPTrunkDispatch(),
   430  		rules: []*livekit.SIPDispatchRuleInfo{
   431  			{TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")},
   432  			{TrunkIds: nil, Rule: newDirectDispatch("sip", "123")},
   433  		},
   434  		expErr:  true,
   435  		invalid: true,
   436  	},
   437  	{
   438  		name:  "direct vs individual/open",
   439  		trunk: newSIPTrunkDispatch(),
   440  		rules: []*livekit.SIPDispatchRuleInfo{
   441  			{TrunkIds: nil, Rule: newIndividualDispatch("pref_", "")},
   442  			{TrunkIds: nil, Rule: newDirectDispatch("sip", "")},
   443  		},
   444  		expErr:  true,
   445  		invalid: true,
   446  	},
   447  	// Direct rules take priority over individual rules.
   448  	{
   449  		name:  "direct vs individual/priority",
   450  		trunk: newSIPTrunkDispatch(),
   451  		rules: []*livekit.SIPDispatchRuleInfo{
   452  			{TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")},
   453  			{TrunkIds: nil, Rule: newDirectDispatch("sip", "456")},
   454  		},
   455  		reqPin: "456",
   456  		exp:    1,
   457  	},
   458  }
   459  
   460  func TestSIPMatchDispatchRule(t *testing.T) {
   461  	for _, c := range dispatchCases {
   462  		c := c
   463  		t.Run(c.name, func(t *testing.T) {
   464  			pins := []string{c.reqPin}
   465  			if !c.expErr && c.reqPin != "" {
   466  				// Should match the same rule, even if no pin is set (so that it can be requested).
   467  				pins = append(pins, "")
   468  			}
   469  			for i, r := range c.rules {
   470  				if r.SipDispatchRuleId == "" {
   471  					r.SipDispatchRuleId = fmt.Sprintf("rule_%d", i)
   472  				}
   473  			}
   474  			for _, pin := range pins {
   475  				pin := pin
   476  				name := pin
   477  				if name == "" {
   478  					name = "no pin"
   479  				}
   480  				t.Run(name, func(t *testing.T) {
   481  					got, err := MatchDispatchRule(c.trunk, c.rules, newSIPReqDispatch(pin, c.noPin))
   482  					if c.expErr {
   483  						require.Error(t, err)
   484  						require.Nil(t, got)
   485  						t.Log(err)
   486  					} else {
   487  						var exp *livekit.SIPDispatchRuleInfo
   488  						if c.exp >= 0 {
   489  							exp = c.rules[c.exp]
   490  						}
   491  						require.NoError(t, err)
   492  						require.Equal(t, exp, got)
   493  					}
   494  				})
   495  			}
   496  		})
   497  	}
   498  }
   499  
   500  func TestSIPValidateDispatchRules(t *testing.T) {
   501  	for _, c := range dispatchCases {
   502  		c := c
   503  		t.Run(c.name, func(t *testing.T) {
   504  			for i, r := range c.rules {
   505  				if r.SipDispatchRuleId == "" {
   506  					r.SipDispatchRuleId = strconv.Itoa(i)
   507  				}
   508  			}
   509  			err := ValidateDispatchRules(c.rules)
   510  			if c.invalid {
   511  				require.Error(t, err)
   512  			} else {
   513  				require.NoError(t, err)
   514  			}
   515  		})
   516  	}
   517  }