diff --git a/samples/SocketModeBot/README.md b/samples/SocketModeBot/README.md
index 2f024ae8..02276a39 100644
--- a/samples/SocketModeBot/README.md
+++ b/samples/SocketModeBot/README.md
@@ -9,8 +9,8 @@ This sample is an echo bot that receives activities over Socket Mode instead of
## What it shows
-- `UseSocketMode()` on `AddTeamsBotApplication` to receive activities over Socket Mode using the default production endpoint (`https://botapi.skype.com`).
-- One connection per geo (`amer`, `emea`, `apac`) by default for inbound coverage across regions. Override with `Geos = [...]` or point at a custom ring with `NegotiateBaseUrl` in the `UseSocketMode(socket => ...)` callback.
+- `UseSocketMode()` on `AddTeamsBotApplication` to receive activities over Socket Mode.
+- One connection per geo (`amer`, `emea`, `apac`) by default for inbound coverage across regions. Configure `Geos = [...]` in the `UseSocketMode(socket => ...)` callback to target specific geos from these three defaults.
- `$(NoWarn);ExperimentalTeamsSocketMode` in the project file, because Socket Mode is experimental.
- A generic host (`Host.CreateApplicationBuilder`) with no web server, and `UseTeamsBotApplication()` to get the app.
- Per-geo `Ready`, `Disconnected`, and `Reconnected` subscriptions in `Program.cs`, plus a status snapshot after startup.
diff --git a/src/Microsoft.Teams.Apps/README.md b/src/Microsoft.Teams.Apps/README.md
index 2f6c29a6..48a83e7e 100644
--- a/src/Microsoft.Teams.Apps/README.md
+++ b/src/Microsoft.Teams.Apps/README.md
@@ -245,7 +245,11 @@ Negotiation failures expose `SocketModeNegotiateException.StatusCode`, `.RetryAf
and `.IsAuthError` for HTTP 401/403. Downstream SignalR negotiation errors are not classified as terminal
service-negotiation rejections.
-`UseSocketMode(configure)` accepts a `SocketModeOptions` callback to change only `NegotiateBaseUrl` and `Geos`.
+`UseSocketMode(configure)` accepts a `SocketModeOptions` callback to change only `Geos`.
+Use this setting to target specific geos from the three defaults: `amer`, `emea`, and `apac`.
+An empty geo list uses all three defaults.
+Each geo's resolved connect/negotiate URL is logged at Information level during startup
+under `Microsoft.Teams.Apps.SocketMode.SocketModeTransport`. The SignalR URL returned by negotiation is used unchanged.
Startup and readiness timeouts and reconnect backoff are managed internally by the SDK.
Keep-alive and server timeout use the SignalR client defaults of 15 and 30 seconds, respectively.
`UseSocketMode(bool)` turns Socket Mode on with the defaults, or off with `false`, which clears any earlier
diff --git a/src/Microsoft.Teams.Apps/SocketMode/GeoSocket.cs b/src/Microsoft.Teams.Apps/SocketMode/GeoSocket.cs
index ce211c99..7ff8be33 100644
--- a/src/Microsoft.Teams.Apps/SocketMode/GeoSocket.cs
+++ b/src/Microsoft.Teams.Apps/SocketMode/GeoSocket.cs
@@ -116,7 +116,7 @@ internal sealed class GeoSocket : IAsyncDisposable
/// Initializes a supervisor for one geo.
///
/// The transport that owns this geo.
- /// The geo identifier. Empty when the negotiate URL has no geo segment.
+ /// The geo identifier.
/// The negotiate endpoint for the geo.
/// Creates one connection per generation.
/// The logger for lifecycle events.
diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs
index c2a5499c..1e664210 100644
--- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs
+++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeObserver.cs
@@ -40,5 +40,6 @@ internal SocketModeObserver(Func getTransport, SocketModeEv
/// Gets the resolved negotiation endpoint for the first configured geo, for diagnostics.
/// Each geo connects to its own endpoint.
///
+ /// This URL is managed by the SDK and is for diagnostics only. Do not depend on it; it may change in the future.
public Uri NegotiateUrl => _transport.Value.NegotiateUrl;
}
diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs
index afdac15b..0b69e41e 100644
--- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs
+++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs
@@ -13,6 +13,7 @@ namespace Microsoft.Teams.Apps.SocketMode;
/// Socket Mode opens one connection per geo and waits until every geo is ready before the host finishes starting.
/// It is supported only in the public cloud, and runs on a host without a web server
/// (Host.CreateApplicationBuilder). Settings are validated when the host starts.
+/// Negotiation URLs are managed by the SDK. Do not depend on these URLs; they may change in the future.
/// Socket Mode is experimental: the API is in preview and may change, and it is recommended only for developing agents.
/// Socket Mode is currently only available for new bots.
///
@@ -20,14 +21,9 @@ namespace Microsoft.Teams.Apps.SocketMode;
public sealed class SocketModeOptions
{
///
- /// Gets or sets the base URL used to negotiate each geo connection. Must use HTTPS unless it targets loopback.
- /// Defaults to https://botapi.skype.com. Override this to target a different Teams backend service ring.
- ///
- public Uri NegotiateBaseUrl { get; set; } = new(SocketModeProtocol.DefaultNegotiateBaseUrl);
-
- ///
- /// Gets or sets the geos to connect, one connection each. An empty string connects to the base URL without a geo
- /// segment. Defaults to amer, emea, and apac. Override this to connect to a different set of geos.
+ /// Gets or sets the geos to connect, one connection each.
+ /// Defaults to amer, emea, and apac, including when the list is empty.
+ /// Override this to connect to a different set of geos.
///
public IReadOnlyList Geos { get; set; } = SocketModeProtocol.DefaultGeos;
@@ -38,7 +34,6 @@ public sealed class SocketModeOptions
/// The transport options.
internal SocketModeTransportOptions ToTransportOptions() => new()
{
- NegotiateBaseUri = NegotiateBaseUrl,
Geos = Geos is null ? null! : [.. Geos],
};
}
diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeProtocol.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeProtocol.cs
index 675df4ba..92b25c99 100644
--- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeProtocol.cs
+++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeProtocol.cs
@@ -14,9 +14,9 @@ internal static class SocketModeProtocol
internal const int CurrentVersion = 1;
///
- /// The default base URL for Socket Mode negotiation.
+ /// The service hostname used for Socket Mode negotiation.
///
- internal const string DefaultNegotiateBaseUrl = "https://botapi.skype.com";
+ internal const string NegotiateHost = "botapi.skype.com";
///
/// The default geographies for Socket Mode connections.
@@ -25,7 +25,7 @@ internal static class SocketModeProtocol
Array.AsReadOnly(["amer", "emea", "apac"]);
///
- /// The negotiate path appended after the geography segment.
+ /// The negotiate path appended to each geo's base URL.
///
internal const string NegotiatePath = "/v3/websockets/connect";
diff --git a/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs b/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs
index da0f72d4..bcf0b5d9 100644
--- a/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs
+++ b/src/Microsoft.Teams.Apps/SocketMode/SocketModeTransport.cs
@@ -48,12 +48,7 @@ public enum SocketModeStatus
internal sealed class SocketModeTransportOptions
{
///
- /// Gets the base URL used to negotiate each geo connection.
- ///
- internal Uri NegotiateBaseUri { get; init; } = new(SocketModeProtocol.DefaultNegotiateBaseUrl);
-
- ///
- /// Gets the geos to connect. An empty string connects to the base URL without a geo segment.
+ /// Gets the geos to connect. An empty list uses the default geos.
///
internal IReadOnlyList Geos { get; init; } = SocketModeProtocol.DefaultGeos;
@@ -117,7 +112,7 @@ internal sealed class SocketModeTransport : IGeoSocketOwner, IAsyncDisposable
/// The time provider for supervision timing and reply timestamps.
/// The random source for reconnect jitter.
/// The optional public lifecycle observers.
- /// Thrown when the geos are empty, null, or duplicated.
+ /// Thrown when the geo list is null or contains null, empty, or duplicate entries.
/// Thrown when the startup timeout is negative.
internal SocketModeTransport(
SocketModeTransportOptions options,
@@ -140,9 +135,8 @@ internal SocketModeTransport(
_random = random ?? Random.Shared;
_events = events;
- ArgumentNullException.ThrowIfNull(options.NegotiateBaseUri, nameof(options));
ArgumentOutOfRangeException.ThrowIfLessThan(options.StartupTimeout, TimeSpan.Zero, nameof(options));
- _geos = ResolveGeos(options.NegotiateBaseUri, options.Geos);
+ _geos = ResolveGeos(options.Geos);
}
///
@@ -241,6 +235,14 @@ internal async Task StartAsync(CancellationToken cancellationToken = default)
geoSockets.Length,
string.Join(", ", geoSockets.Select(geoSocket => geoSocket.Geo)));
+ foreach (var geo in _geos)
+ {
+ _logger.LogInformation(
+ "Socket Mode geo {Geo} negotiate URL {NegotiateUrl}.",
+ geo.Geo,
+ geo.NegotiateUri);
+ }
+
using CancellationTokenSource startSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
Exception? firstFailure = null;
@@ -494,17 +496,28 @@ private static bool IsInvoke(SocketActivityEnvelope envelope, CoreActivity activ
TeamsActivityTypes.Invoke,
StringComparison.OrdinalIgnoreCase);
- private static List<(string Geo, Uri NegotiateUri)> ResolveGeos(Uri baseUri, IReadOnlyList geos)
+ private static List<(string Geo, Uri NegotiateUri)> ResolveGeos(IReadOnlyList geos)
{
- if (geos is null || geos.Count == 0)
+ if (geos is null)
+ {
+ throw new ArgumentException("Socket Mode geos cannot be null.", nameof(geos));
+ }
+
+ if (geos.Count == 0)
{
- throw new ArgumentException(
- "Socket Mode requires at least one geo. Use an empty string to connect without a geo segment.",
- nameof(geos));
+ geos = SocketModeProtocol.DefaultGeos;
+ }
+
+ if (geos.Contains(string.Empty))
+ {
+ throw new ArgumentException("Socket Mode geos must not contain an empty string.", nameof(geos));
+ }
+
+ if (new HashSet(geos, StringComparer.Ordinal).Count != geos.Count)
+ {
+ throw new ArgumentException("Socket Mode geos must not contain duplicates.", nameof(geos));
}
- string baseUrl = baseUri.AbsoluteUri.TrimEnd('/');
- HashSet seen = new(StringComparer.OrdinalIgnoreCase);
List<(string Geo, Uri NegotiateUri)> resolved = [];
foreach (string geo in geos)
@@ -514,16 +527,11 @@ private static bool IsInvoke(SocketActivityEnvelope envelope, CoreActivity activ
throw new ArgumentException("Socket Mode geos cannot contain null.", nameof(geos));
}
- string segment = geo.Trim().Trim('/');
- if (!seen.Add(segment))
+ UriBuilder builder = new(Uri.UriSchemeHttps, $"{Uri.EscapeDataString(geo)}.{SocketModeProtocol.NegotiateHost}")
{
- throw new ArgumentException($"Socket Mode geo '{geo}' is listed more than once.", nameof(geos));
- }
-
- string path = segment.Length > 0
- ? $"/{Uri.EscapeDataString(segment)}{SocketModeProtocol.NegotiatePath}"
- : SocketModeProtocol.NegotiatePath;
- resolved.Add((segment, new Uri(baseUrl + path, UriKind.Absolute)));
+ Path = SocketModeProtocol.NegotiatePath,
+ };
+ resolved.Add((geo, builder.Uri));
}
return resolved;
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/GeoSocketTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/GeoSocketTests.cs
index 8ac4ff59..31229dbb 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/GeoSocketTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/GeoSocketTests.cs
@@ -10,7 +10,7 @@ namespace Microsoft.Teams.Apps.UnitTests.SocketMode;
public class GeoSocketTests
{
- private static readonly Uri NegotiateUri = new("https://botapi.skype.com/amer/v3/websockets/connect");
+ private static readonly Uri NegotiateUri = new("https://amer.botapi.skype.com/v3/websockets/connect");
[Fact]
public async Task StartAsync_CompletesOnlyAfterSocketReady()
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SignalRSocketConnectionTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SignalRSocketConnectionTests.cs
index 44703549..1ab7eec1 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SignalRSocketConnectionTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SignalRSocketConnectionTests.cs
@@ -9,9 +9,9 @@ namespace Microsoft.Teams.Apps.UnitTests.SocketMode;
public class SignalRSocketConnectionTests
{
private static readonly Uri NegotiateUri =
- new("https://botapi.skype.com/amer/v3/websockets/connect");
+ new("https://amer.botapi.skype.com/v3/websockets/connect");
private static readonly Uri SignalRUri =
- new("https://signalr.example.test/client");
+ new("https://signalr.example.test/client?hub=teams");
[Fact]
public async Task StartAsync_NegotiatesCreatesAndStartsConnection()
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeEndToEndTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeEndToEndTests.cs
index e90db095..da6d12f8 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeEndToEndTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeEndToEndTests.cs
@@ -34,9 +34,9 @@ public async Task Transport_ComposesNegotiatorAndSignalRFactoryAcrossGeos()
Assert.Equal(SocketModeStatus.Ready, stack.Transport.Status);
Assert.Equal(
[
- "https://botapi.skype.com/amer/v3/websockets/connect",
- "https://botapi.skype.com/apac/v3/websockets/connect",
- "https://botapi.skype.com/emea/v3/websockets/connect",
+ "https://amer.botapi.skype.com/v3/websockets/connect",
+ "https://apac.botapi.skype.com/v3/websockets/connect",
+ "https://emea.botapi.skype.com/v3/websockets/connect",
],
stack.Http.Requests.Select(request => request.AbsoluteUri).Order(StringComparer.Ordinal));
Assert.All(stack.Http.Authorizations, authorization => Assert.Equal("Bearer bot-token", authorization));
@@ -168,7 +168,7 @@ protected override Task SendAsync(
return Task.FromResult(throttled);
}
- string geo = uri.Segments[1].TrimEnd('/');
+ string geo = uri.Host.Split('.')[0];
string json = $$"""{"url":"https://signalr.test/{{geo}}","accessToken":"token-{{geo}}","expiresIn":3600}""";
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs
index 13a2bbdd..8168ae5b 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeHostingTests.cs
@@ -94,9 +94,11 @@ public void WithSocketMode_AcceptsPublicIssuerWithTrailingSlash()
public static TheoryData> InvalidOptions => new()
{
- o => o.Geos = [],
+ o => o.Geos = null!,
+ o => o.Geos = [""],
+ o => o.Geos = ["amer", ""],
o => o.Geos = ["amer", null!],
- o => o.Geos = ["amer", " AMER/"],
+ o => o.Geos = ["amer", "amer"],
};
[Theory]
@@ -135,7 +137,7 @@ public async Task Negotiator_UsesTheBotTokenForTheBotFrameworkScopeOnAPlainClien
using ServiceProvider provider = services.BuildServiceProvider();
SocketModeNegotiateResponse response = await provider.GetRequiredService()
- .NegotiateAsync(new Uri("https://botapi.skype.com/amer/v3/websockets/connect"));
+ .NegotiateAsync(new Uri("https://amer.botapi.skype.com/v3/websockets/connect"));
Assert.Equal("signalr-token", response.AccessToken);
Assert.Equal(["https://api.botframework.com/.default"], scopes);
@@ -165,7 +167,7 @@ public async Task HostedService_StartsEveryGeoDispatchesToTheAppAndStops()
SocketModeTransport transport = provider.GetRequiredService();
Assert.Equal(SocketModeStatus.Idle, socket.Status);
Assert.Equal(["amer", "emea"], socket.GeoList);
- Assert.Equal(new Uri("https://botapi.skype.com/amer/v3/websockets/connect"), socket.NegotiateUrl);
+ Assert.Equal(new Uri("https://amer.botapi.skype.com/v3/websockets/connect"), socket.NegotiateUrl);
Assert.Empty(socket.GeoStatuses);
await hosted.StartAsync(CancellationToken.None);
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeNegotiatorTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeNegotiatorTests.cs
index e2c5b3da..06e9eb73 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeNegotiatorTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeNegotiatorTests.cs
@@ -11,30 +11,12 @@ namespace Microsoft.Teams.Apps.UnitTests.SocketMode;
public class SocketModeNegotiatorTests
{
private const string BotToken = "bot-token";
- private static readonly Uri NegotiateUri = new("https://botapi.skype.com/amer/v3/websockets/connect");
+ private static readonly Uri NegotiateUri = new("https://amer.botapi.skype.com/v3/websockets/connect");
[Theory]
- [InlineData("https://botapi.skype.com/v3/websockets/connect")]
- [InlineData("http://localhost:5000/v3/websockets/connect")]
- [InlineData("http://127.0.0.1:5000/v3/websockets/connect")]
- [InlineData("http://[::1]:5000/v3/websockets/connect")]
- public async Task NegotiateAsync_AllowsSecureAndLoopbackUris(string uri)
- {
- RecordingHandler handler = SuccessHandler();
- SocketModeNegotiator negotiator = CreateNegotiator(handler);
-
- SocketModeNegotiateResponse response = await negotiator.NegotiateAsync(new Uri(uri));
-
- Assert.Equal("https://signalr.example.test/client", response.Url);
- Assert.Equal(1, handler.SendCount);
- }
-
- [Theory]
- [InlineData("http://botapi.skype.com/v3/websockets/connect")]
- [InlineData("http://127.0.0.2/v3/websockets/connect")]
- [InlineData("wss://botapi.skype.com/v3/websockets/connect")]
- [InlineData("ws://localhost:5000/v3/websockets/connect")]
- [InlineData("ftp://botapi.skype.com/v3/websockets/connect")]
+ [InlineData("http://amer.botapi.skype.com/v3/websockets/connect")]
+ [InlineData("wss://amer.botapi.skype.com/v3/websockets/connect")]
+ [InlineData("ftp://amer.botapi.skype.com/v3/websockets/connect")]
public async Task NegotiateAsync_RejectsUnsupportedNegotiateUris(string uri)
{
RecordingHandler handler = SuccessHandler();
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs
index 96fb6f3d..4549517e 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeObserverTests.cs
@@ -16,8 +16,7 @@ public async Task Diagnostics_AreLazyAndDoNotConnect()
await using SocketModeTransport transport = new(
new SocketModeTransportOptions
{
- NegotiateBaseUri = new Uri("https://canary.botapi.skype.com/"),
- Geos = [" /eu/ "],
+ Geos = ["emea"],
},
new UnusedConnectionFactory(),
_ => Task.FromResult(new SocketDispatchResult(200)),
@@ -32,8 +31,8 @@ public async Task Diagnostics_AreLazyAndDoNotConnect()
observer.Events.Ready += (_, _) => { };
Assert.Equal(0, created);
Assert.Equal(SocketModeStatus.Idle, observer.Status);
- Assert.Equal(new Uri("https://canary.botapi.skype.com/eu/v3/websockets/connect"), observer.NegotiateUrl);
- Assert.Equal(["eu"], observer.GeoList);
+ Assert.Equal(new Uri("https://emea.botapi.skype.com/v3/websockets/connect"), observer.NegotiateUrl);
+ Assert.Equal(["emea"], observer.GeoList);
Assert.Empty(observer.GeoStatuses);
Assert.Equal(1, created);
}
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeOptionsTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeOptionsTests.cs
index 01a2167a..ced706ef 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeOptionsTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeOptionsTests.cs
@@ -8,13 +8,13 @@ namespace Microsoft.Teams.Apps.UnitTests.SocketMode;
public class SocketModeOptionsTests
{
[Fact]
- public void Properties_ContainOnlyNegotiateBaseUrlAndGeos()
+ public void Properties_ContainOnlyGeos()
{
var properties = typeof(SocketModeOptions).GetProperties(
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
Assert.Equal(
- [nameof(SocketModeOptions.Geos), nameof(SocketModeOptions.NegotiateBaseUrl)],
+ [nameof(SocketModeOptions.Geos)],
properties.Select(property => property.Name).OrderBy(name => name, StringComparer.Ordinal));
Assert.All(properties, property =>
{
@@ -28,7 +28,6 @@ public void Defaults_MatchTheOtherSdks()
{
SocketModeOptions options = new();
- Assert.Equal(new Uri("https://botapi.skype.com"), options.NegotiateBaseUrl);
Assert.Equal(["amer", "emea", "apac"], options.Geos);
SocketModeTransportOptions transport = options.ToTransportOptions();
Assert.Equal(TimeSpan.FromSeconds(30), transport.StartupTimeout);
@@ -45,19 +44,17 @@ public void ToTransportOptions_PassesInvalidValuesThroughForTheTransportToReject
}
[Fact]
- public void ToTransportOptions_CopiesEndpointAndSnapshotsGeos()
+ public void ToTransportOptions_SnapshotsGeos()
{
List geos = ["emea"];
SocketModeOptions options = new()
{
- NegotiateBaseUrl = new Uri("https://example.test"),
Geos = geos,
};
SocketModeTransportOptions transport = options.ToTransportOptions();
geos.Add("apac");
- Assert.Equal(new Uri("https://example.test"), transport.NegotiateBaseUri);
Assert.Equal(["emea"], transport.Geos);
}
diff --git a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs
index d6f92f53..8366da10 100644
--- a/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs
+++ b/test/Microsoft.Teams.Apps.UnitTests/SocketMode/SocketModeTransportTests.cs
@@ -16,19 +16,21 @@ public class SocketModeTransportTests
{
private static readonly DateTimeOffset Now = DateTimeOffset.FromUnixTimeMilliseconds(1_700_000_000_000);
- [Fact]
- public async Task StartAsync_ConnectsEveryDefaultGeo()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task StartAsync_ConnectsEveryDefaultGeo(bool emptyGeos)
{
- Harness harness = new();
+ Harness harness = new(emptyGeos ? new SocketModeTransportOptions { Geos = [] } : null);
Task start = harness.Transport.StartAsync();
FakeConnection[] connections = await harness.Factory.WaitForAsync(3);
Assert.Equal(
[
- "https://botapi.skype.com/amer/v3/websockets/connect",
- "https://botapi.skype.com/apac/v3/websockets/connect",
- "https://botapi.skype.com/emea/v3/websockets/connect",
+ "https://amer.botapi.skype.com/v3/websockets/connect",
+ "https://apac.botapi.skype.com/v3/websockets/connect",
+ "https://emea.botapi.skype.com/v3/websockets/connect",
],
SortedUris(connections));
Assert.False(start.IsCompleted);
@@ -42,40 +44,48 @@ public async Task StartAsync_ConnectsEveryDefaultGeo()
await start;
Assert.Equal(SocketModeStatus.Ready, harness.Transport.Status);
Assert.All(harness.Transport.GeoStatuses.Values, status => Assert.Equal(SocketModeStatus.Ready, status));
+ await harness.Transport.DisposeAsync();
}
- [Fact]
- public async Task StartAsync_BuildsGeoUrisFromCustomBaseAndEmptyGeo()
+ [Theory]
+ [InlineData(new object[] { new[] { "emea" } })]
+ [InlineData(new object[] { new[] { "custom-geo", "other-geo" } })]
+ [InlineData(new object[] { new[] { "amer", "AMER" } })]
+ [InlineData(new object[] { new[] { "amer.amer" } })]
+ public async Task StartAsync_ConnectsConfiguredGeos(string[] geos)
{
Harness harness = new(new SocketModeTransportOptions
{
- NegotiateBaseUri = new Uri("http://localhost:3978/"),
- Geos = ["", " /eu/ "],
+ Geos = geos,
});
- Task start = harness.Transport.StartAsync();
- FakeConnection[] connections = await harness.Factory.WaitForAsync(2);
+ FakeConnection[] connections = await harness.StartReadyAsync();
Assert.Equal(
- [
- "http://localhost:3978/eu/v3/websockets/connect",
- "http://localhost:3978/v3/websockets/connect",
- ],
+ geos.Select(geo => $"https://{geo.ToLowerInvariant()}.botapi.skype.com/v3/websockets/connect").Order(StringComparer.Ordinal),
SortedUris(connections));
- foreach (FakeConnection connection in connections)
- {
- connection.Ready();
- }
-
- await start;
+ Assert.Equal(geos, harness.Transport.GeoList);
+ Assert.Equal(geos.Length, harness.Transport.GeoStatuses.Count);
+ Assert.All(harness.Transport.GeoStatuses.Values, status => Assert.Equal(SocketModeStatus.Ready, status));
+ await harness.Transport.DisposeAsync();
}
[Theory]
- [InlineData(new object[] { new string[0] })]
- [InlineData(new object[] { new[] { "amer", "AMER" } })]
- public void Constructor_RejectsInvalidGeos(string[] geos)
+ [InlineData(null)]
+ [InlineData(new object[] { new string[] { null! } })]
+ [InlineData(new object[] { new[] { "" } })]
+ [InlineData(new object[] { new[] { "amer", "" } })]
+ [InlineData(new object[] { new[] { "amer", "amer" } })]
+ public void Constructor_RejectsInvalidGeos(string[]? geos)
+ {
+ Assert.Throws(() => new Harness(new SocketModeTransportOptions { Geos = geos! }).Transport);
+ }
+
+ [Fact]
+ public void Constructor_DoesNotInterpretGeoAsUrlSyntax()
{
- Assert.Throws(() => new Harness(new SocketModeTransportOptions { Geos = geos }).Transport);
+ Assert.Throws(() =>
+ new Harness(new SocketModeTransportOptions { Geos = ["attacker.example#"] }).Transport);
}
[Fact]
@@ -891,8 +901,7 @@ internal async Task WaitForAsync(int count)
public ISocketConnection Create(Uri negotiateUri, SocketConnectionHandlers handlers, Func? getReplacedConnectionId = null)
{
- string[] segments = negotiateUri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
- string geo = segments.Length > 3 ? segments[0] : string.Empty;
+ string geo = negotiateUri.Host.Split('.')[0];
FakeConnection connection = new(
negotiateUri,
geo,