diff --git a/LivekitApi.Tests/IngressServiceClient.Test.cs b/LivekitApi.Tests/IngressServiceClient.Test.cs index 330b0e2..9303c23 100644 --- a/LivekitApi.Tests/IngressServiceClient.Test.cs +++ b/LivekitApi.Tests/IngressServiceClient.Test.cs @@ -47,6 +47,7 @@ public async Task Create_Ingress_Url() ParticipantName = "ingress-name", InputType = IngressInput.UrlInput, Url = url, + Enabled = true, } ); Assert.NotNull(ingress.IngressId); diff --git a/LivekitApi.Tests/ServiceClientFixture.cs b/LivekitApi.Tests/ServiceClientFixture.cs index ebe7ba6..c27d101 100644 --- a/LivekitApi.Tests/ServiceClientFixture.cs +++ b/LivekitApi.Tests/ServiceClientFixture.cs @@ -21,6 +21,7 @@ public class ServiceClientFixture : IDisposable private const string LIVEKIT_SERVER_IMAGE = "livekit/livekit-server:latest"; private const string LIVEKIT_EGRESS_IMAGE = "livekit/egress:latest"; private const string LIVEKIT_INGRESS_IMAGE = "livekit/ingress:latest"; + private const string LIVEKIT_SIP_IMAGE = "livekit/sip:latest"; private const string LIVEKIT_CLI_IMAGE = "livekit/livekit-cli:latest"; private const string REDIS_IMAGE = "redis:latest"; @@ -52,10 +53,24 @@ public class ServiceClientFixture : IDisposable http_relay_port: 9090 health_port: 9091"; + private string sipYaml = + @"api_key: " + + TEST_API_KEY + + @" +api_secret: " + + TEST_API_SECRET + + @" +ws_url: {WS_URL} +redis: + address: {REDIS_ADDRESS} +sip_port: 5060 +rtp_port: 10000-20000"; + private IContainer redisContainer; private IContainer livekitServerContainer; private IContainer egressContainer; private IContainer ingressContainer; + private IContainer sipContainer; public ServiceClientFixture() { @@ -136,7 +151,28 @@ public ServiceClientFixture() ) ) .Build(); - Task.WaitAll(egressContainer.StartAsync(), ingressContainer.StartAsync()); + // Sip + sipYaml = sipYaml + .Replace("{WS_URL}", "ws://" + livekitServerContainer.IpAddress + ":7880") + .Replace("{REDIS_ADDRESS}", redisContainer.IpAddress + ":6379"); + sipContainer = new ContainerBuilder() + .WithImage(LIVEKIT_SIP_IMAGE) + .WithName("sip") + .WithAutoRemove(true) + .WithEnvironment("SIP_CONFIG_BODY", sipYaml) + .WithPortBinding(5060, true) + .DependsOn(redisContainer) + .DependsOn(livekitServerContainer) + .WithWaitStrategy( + Wait.ForUnixContainer().UntilMessageIsLogged(".*sip signaling listening on.*") + ) + .Build(); + + Task.WaitAll( + egressContainer.StartAsync(), + ingressContainer.StartAsync(), + sipContainer.StartAsync() + ); } public void Dispose() @@ -158,6 +194,10 @@ public void Dispose() { tasks.Add(ingressContainer.DisposeAsync().AsTask()); } + if (sipContainer != null) + { + tasks.Add(sipContainer.DisposeAsync().AsTask()); + } Task.WhenAll(tasks).Wait(); } diff --git a/LivekitApi.Tests/SipServiceClient.Test.cs b/LivekitApi.Tests/SipServiceClient.Test.cs index 279a3e2..46fc0fa 100644 --- a/LivekitApi.Tests/SipServiceClient.Test.cs +++ b/LivekitApi.Tests/SipServiceClient.Test.cs @@ -1,4 +1,7 @@ using Google.Protobuf; +using Google.Protobuf.Collections; +using Xunit; +using Xunit.Abstractions; namespace Livekit.Server.Sdk.Dotnet.Test { @@ -6,10 +9,12 @@ namespace Livekit.Server.Sdk.Dotnet.Test public class SipServiceClientTest { private ServiceClientFixture fixture; + private readonly ITestOutputHelper output; - public SipServiceClientTest(ServiceClientFixture fixture) + public SipServiceClientTest(ServiceClientFixture fixture, ITestOutputHelper output) { this.fixture = fixture; + this.output = output; } private SipServiceClient sipClient = new SipServiceClient( @@ -17,6 +22,11 @@ public SipServiceClientTest(ServiceClientFixture fixture) ServiceClientFixture.TEST_API_KEY, ServiceClientFixture.TEST_API_SECRET ); + private readonly RoomServiceClient roomClient = new RoomServiceClient( + ServiceClientFixture.TEST_HTTP_URL, + ServiceClientFixture.TEST_API_KEY, + ServiceClientFixture.TEST_API_SECRET + ); [Fact] [Trait("Category", "Integration")] @@ -67,6 +77,23 @@ public async Task Create_Sip_Inbound_Trunk() Assert.Equal(request.Trunk.AllowedNumbers, response.AllowedNumbers); } + [Fact] + [Trait("Category", "Integration")] + [Trait("Category", "SipService")] + public async Task Create_Sip_Inbound_Trunk_Exceptions() + { + Twirp.Exception ex = await Assert.ThrowsAsync( + async () => + await sipClient.CreateSIPInboundTrunk( + new CreateSIPInboundTrunkRequest { Trunk = new SIPInboundTrunkInfo { } } + ) + ); + Assert.Equal( + "for security, one of the fields must be set: AuthUsername+AuthPassword, AllowedAddresses or Numbers", + ex.Message + ); + } + [Fact] [Trait("Category", "Integration")] [Trait("Category", "SipService")] @@ -92,13 +119,50 @@ public async Task Create_Sip_Outbound_Trunk() Assert.Equal(request.Trunk.AuthPassword, response.AuthPassword); } + [Fact] + [Trait("Category", "Integration")] + [Trait("Category", "SipService")] + public async Task Create_Sip_Outbound_Trunk_Exceptions() + { + Twirp.Exception ex = await Assert.ThrowsAsync( + async () => + await sipClient.CreateSIPOutboundTrunk( + new CreateSIPOutboundTrunkRequest { Trunk = new SIPOutboundTrunkInfo { } } + ) + ); + Assert.Equal("no trunk numbers specified", ex.Message); + ex = await Assert.ThrowsAsync( + async () => + await sipClient.CreateSIPOutboundTrunk( + new CreateSIPOutboundTrunkRequest + { + Trunk = new SIPOutboundTrunkInfo { Address = "my-test-trunk.com" }, + } + ) + ); + Assert.Equal("no trunk numbers specified", ex.Message); + ex = await Assert.ThrowsAsync( + async () => + await sipClient.CreateSIPOutboundTrunk( + new CreateSIPOutboundTrunkRequest + { + Trunk = new SIPOutboundTrunkInfo { Numbers = { "+111", "+222" } }, + } + ) + ); + Assert.Equal("no outbound address specified", ex.Message); + } + [Fact] [Trait("Category", "Integration")] [Trait("Category", "SipService")] public async Task Get_Sip_Inbound_Trunk() { var inboundTrunk = await sipClient.CreateSIPInboundTrunk( - new CreateSIPInboundTrunkRequest { Trunk = new SIPInboundTrunkInfo { } } + new CreateSIPInboundTrunkRequest + { + Trunk = new SIPInboundTrunkInfo { Numbers = { "+111", "+222" } }, + } ); var getRequest = new GetSIPInboundTrunkRequest { SipTrunkId = inboundTrunk.SipTrunkId }; var response = await sipClient.GetSIPInboundTrunk(getRequest); @@ -112,7 +176,14 @@ public async Task Get_Sip_Inbound_Trunk() public async Task Get_Sip_Outound_Trunk() { var outboundTrunk = await sipClient.CreateSIPOutboundTrunk( - new CreateSIPOutboundTrunkRequest { Trunk = new SIPOutboundTrunkInfo { } } + new CreateSIPOutboundTrunkRequest + { + Trunk = new SIPOutboundTrunkInfo + { + Numbers = { "+111", "+222" }, + Address = "my-test-trunk.com", + }, + } ); var getRequest = new GetSIPOutboundTrunkRequest { @@ -129,7 +200,10 @@ public async Task Get_Sip_Outound_Trunk() public async Task Delete_Sip_Trunk() { var trunk = await sipClient.CreateSIPInboundTrunk( - new CreateSIPInboundTrunkRequest { Trunk = new SIPInboundTrunkInfo { } } + new CreateSIPInboundTrunkRequest + { + Trunk = new SIPInboundTrunkInfo { Numbers = { "+111", "+222" } }, + } ); var allTrunks = await sipClient.ListSIPInboundTrunk(new ListSIPInboundTrunkRequest { }); Assert.Contains(allTrunks.Items, t => t.SipTrunkId == trunk.SipTrunkId); @@ -185,5 +259,162 @@ public async Task Dispatch_Rule() r => r.SipDispatchRuleId == dispatchRule.SipDispatchRuleId ); } + + [Fact] + [Trait("Category", "Integration")] + [Trait("Category", "SipService")] + public async Task Create_Sip_Participant() + { + Twirp.Exception ex = await Assert.ThrowsAsync( + async () => + await sipClient.CreateSIPParticipant( + new CreateSIPParticipantRequest { SipTrunkId = "non-existing-trunk" } + ) + ); + Assert.Equal("requested sip trunk does not exist", ex.Message); + + SIPOutboundTrunkInfo trunk = await sipClient.CreateSIPOutboundTrunk( + new CreateSIPOutboundTrunkRequest + { + Trunk = new SIPOutboundTrunkInfo + { + Name = "Demo outbound trunk", + Address = "my-test-trunk.com", + Numbers = { "+1234567890" }, + AuthUsername = "username", + AuthPassword = "password", + }, + } + ); + + CreateSIPParticipantRequest request = new CreateSIPParticipantRequest + { + SipTrunkId = "trunk", + }; + + ex = await Assert.ThrowsAsync( + async () => + await sipClient.CreateSIPParticipant( + new CreateSIPParticipantRequest { SipTrunkId = "non-existing-trunk" } + ) + ); + Assert.Equal("requested sip trunk does not exist", ex.Message); + + request.SipTrunkId = trunk.SipTrunkId; + + ex = await Assert.ThrowsAsync( + async () => await sipClient.CreateSIPParticipant(request) + ); + Assert.Equal("call-to number must be set", ex.Message); + + request.SipCallTo = "+3333"; + + ex = await Assert.ThrowsAsync( + async () => await sipClient.CreateSIPParticipant(request) + ); + Assert.Equal("room name must be set", ex.Message); + + request.RoomName = TestConstants.ROOM_NAME; + + ex = await Assert.ThrowsAsync( + async () => await sipClient.CreateSIPParticipant(request) + ); + Assert.Equal("update room failed: identity cannot be empty", ex.Message); + + request.ParticipantIdentity = TestConstants.PARTICIPANT_IDENTITY; + + request.ParticipantName = "Test Caller"; + request.SipNumber = "+1111"; + request.RingingTimeout = new Google.Protobuf.WellKnownTypes.Duration { Seconds = 10 }; + request.PlayDialtone = true; + request.ParticipantMetadata = "meta"; + request.ParticipantAttributes.Add("extra", "1"); + request.MediaEncryption = SIPMediaEncryption.SipMediaEncryptRequire; + request.MaxCallDuration = new Google.Protobuf.WellKnownTypes.Duration { Seconds = 99 }; + request.KrispEnabled = true; + request.IncludeHeaders = SIPHeaderOptions.SipAllHeaders; + request.Dtmf = "1234#"; + request.HidePhoneNumber = true; + request.Headers.Add("X-A", "A"); + + SIPParticipantInfo sipParticipantInfo = await sipClient.CreateSIPParticipant(request); + + Assert.NotNull(sipParticipantInfo); + Assert.Equal(TestConstants.PARTICIPANT_IDENTITY, request.ParticipantIdentity); + Assert.Equal(TestConstants.ROOM_NAME, request.RoomName); + + var sipParticipant = await roomClient.GetParticipant( + new RoomParticipantIdentity + { + Room = TestConstants.ROOM_NAME, + Identity = TestConstants.PARTICIPANT_IDENTITY, + } + ); + Assert.NotNull(sipParticipant); + Assert.Equal(TestConstants.PARTICIPANT_IDENTITY, sipParticipant.Identity); + Assert.Equal("Test Caller", sipParticipant.Name); + Assert.Equal("meta", sipParticipant.Metadata); + Assert.Equal("1", sipParticipant.Attributes["extra"]); + } + + [Fact] + [Trait("Category", "Integration")] + [Trait("Category", "SipService")] + public async Task Transfer_Sip_Participant() + { + var trunk = await sipClient.CreateSIPOutboundTrunk( + new CreateSIPOutboundTrunkRequest + { + Trunk = new SIPOutboundTrunkInfo + { + Name = "Demo outbound trunk", + Address = "my-test-trunk.com", + Numbers = { "+1234567890" }, + AuthUsername = "username", + AuthPassword = "password", + }, + } + ); + + var request = new CreateSIPParticipantRequest + { + SipTrunkId = trunk.SipTrunkId, + SipCallTo = "+3333", + RoomName = TestConstants.ROOM_NAME, + ParticipantIdentity = TestConstants.PARTICIPANT_IDENTITY, + ParticipantName = "Test Caller", + SipNumber = "+1111", + }; + + SIPParticipantInfo sipParticipantInfo = await sipClient.CreateSIPParticipant(request); + + var transferRequest = new TransferSIPParticipantRequest { }; + + Twirp.Exception ex = await Assert.ThrowsAsync( + async () => await sipClient.TransferSIPParticipant(transferRequest) + ); + Assert.Equal("Missing room name", ex.Message); + + transferRequest.RoomName = TestConstants.ROOM_NAME; + + ex = await Assert.ThrowsAsync( + async () => await sipClient.TransferSIPParticipant(transferRequest) + ); + Assert.Equal("Missing participant identity", ex.Message); + + transferRequest.ParticipantIdentity = TestConstants.PARTICIPANT_IDENTITY; + transferRequest.TransferTo = "+14155550100"; + transferRequest.PlayDialtone = false; + + ex = await Assert.ThrowsAsync( + async () => await sipClient.TransferSIPParticipant(transferRequest) + ); + Assert.Equal("can't transfer non established call", ex.Message); + + ex = await Assert.ThrowsAsync( + async () => await sipClient.TransferSIPParticipant(transferRequest) + ); + Assert.Equal("participant does not exist", ex.Message); + } } } diff --git a/LivekitApi/SipServiceClient.cs b/LivekitApi/SipServiceClient.cs index 6bbd030..a8809fd 100644 --- a/LivekitApi/SipServiceClient.cs +++ b/LivekitApi/SipServiceClient.cs @@ -132,7 +132,7 @@ CreateSIPParticipantRequest request { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", - AuthHeader(new VideoGrants { }, new SIPGrants { Admin = true }) + AuthHeader(new VideoGrants { }, new SIPGrants { Call = true }) ); return await Twirp.CreateSIPParticipant(httpClient, request); } @@ -141,7 +141,10 @@ public async Task TransferSIPParticipant(TransferSIPParticipantRequest re { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Bearer", - AuthHeader(new VideoGrants { }, new SIPGrants { Admin = true }) + AuthHeader( + new VideoGrants { RoomAdmin = true, Room = request.RoomName }, + new SIPGrants { Call = true } + ) ); return await Twirp.TransferSIPParticipant(httpClient, request); }