From b7b0f4a3c6cd45008062ee6b83bea48e81c5177b Mon Sep 17 00:00:00 2001 From: jericho Date: Sat, 5 Aug 2023 14:10:30 -0400 Subject: [PATCH 1/5] Remove unused 'using' statements --- Source/ZoomNet/Resources/Chatbot.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/ZoomNet/Resources/Chatbot.cs b/Source/ZoomNet/Resources/Chatbot.cs index 7ec8a5a5..c1719afe 100644 --- a/Source/ZoomNet/Resources/Chatbot.cs +++ b/Source/ZoomNet/Resources/Chatbot.cs @@ -2,7 +2,6 @@ using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; using ZoomNet.Models; using ZoomNet.Models.ChatbotMessage; From 54ee66e3afe17bd6107a9814306d8cd3d571c9aa Mon Sep 17 00:00:00 2001 From: jericho Date: Sun, 6 Aug 2023 12:51:00 -0400 Subject: [PATCH 2/5] (GH-313) Readme to explain how to configure integration tests --- Source/ZoomNet.IntegrationTests/README.md | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Source/ZoomNet.IntegrationTests/README.md diff --git a/Source/ZoomNet.IntegrationTests/README.md b/Source/ZoomNet.IntegrationTests/README.md new file mode 100644 index 00000000..75d4d6e9 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/README.md @@ -0,0 +1,82 @@ +# ZoomNet integration tests + +Integration tests allow you to run live tests against the Zoom system to test the ZoomNet library. There are three main scenarios (we call the "suites"): +- General API tests +- Chatbot tests +- WebSocket client tests + + +## Configuration + +Before you can run the integration tests, there are a few settings you must configure in addition to a few environment variables you must set. + +### Step 1: configure your proxy tool + +```csharp +// Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. +var useProxy = true; +``` + +Line 38 (shown above) in `TestsRunner.cs` allows you to indicate whether you want to send all HTTP requests through a proxy running on your machine such as [Telerik's Fiddler](https://www.telerik.com/fiddler) for example. +I mention Fiddler simply because it's my preferred proxy tool, but feel free to use an alternative tool if you prefer. +By the way, there are two versions of Fiddler available for download on Telerik's web site: "Fiddler Everywhere" and "Fiddler Classic". +Both are very capable proxys but I personally prefer the classic version, therefore this is the one I recommend. +A proxy tool is very helpful because it allows you to see the content of each request and the content of their corresponding response from the Zoom API. This is helpful to investigate situations where you are not getting the result you were expecting. Providing the content of the request and/or the response is + +```csharp +// By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 +var proxyPort = 8888; +``` + +Line 41 (shown above) in `TestsRunner.cs` allows you to specify the port number used by your proxy tools. For instance, Fiddler classic uses port 8888 by default while Fiddler Everywhere uses 8886 by default. +Most proxys allow you to customize the port number if, for some reason, you are not satisfied with their default. Feel free to customize this port number if desired but make sure to update the `proxyPort` value accordingly. + +> :warning: You will get a `No connection could be made because the target machine actively refused it` exception when attempting to run the integration tests if you configure `useProxy` to `true` and your proxy is not running on you machine or the `proxyPort` value does not correspond to the port used by your proxy. + +### Step 2: configure which test "suite" you want to run + +```csharp +// What tests do you want to run +var testType = TestType.Api; +``` + +Line 44 (shown above) in `TestsRunner.cs` allows you to specify which of the test suites you want to run. As of this writing, there are three suites to choose from: the first only allows you to invoke a multitude of endpoints in the Zoom RST API, the second one allows you to test API calls that are intended to be invoked by a Chatbot app and the third one allows you to receive and parse webhook sent to you by Zoom via websockets. + + +### Step 3: configure which authentication flow you want to use + +```csharp +// Which connection type do you want to use? +var connectionType = ConnectionType.OAuthServerToServer; +``` + +Line 47 (shown above) in `TestsRunner.cs` allows you to specify which authentication flow you want to use. + +> :warning: ZoomNet allows you to select JWT as your authentication flow but keep in mind that Zoom has announced they are retiring this authentication mechanism and the projected end-of-life for JWT apps is September 1, 2023. + +### Step 4: environment variables + +The ZoomNet integration tests rely on various environment variables to store values that are specific to your environment, such as your client id and client secret for example. The exact list and name of these environment variables vary depending on the authentication flow you selected in "Step 3". The chart below lists all the environment variables for each connection type: + +| Connection Type | Environment variables | +|----------|-------------------| +| JWT | ZOOM_JWT_APIKEY
ZOOM_JWT_APISECRET | +| OAuthAuthorizationCode | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_AUTHORIZATIONCODE
**Note** the authorization code is intended to be used only once therefore the environment variable is cleared after the first use and a refresh token is used subsequently | +| OAuthRefreshToken | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_REFRESHTOKEN
**Note** The refresh token is initially created when an authorization code is used | +| OAuthClientCredentials | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN *(optional)*
**Note** If the access token is omitted, a new one will be requested and the environment variable will be updated accordingly | +| OAuthServerToServer | ZOOM_OAUTH_CLIENTID
ZOOM_OAUTH_CLIENTSECRET
ZOOM_OAUTH_ACCOUNTID
ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN *(optional)*
**Note** If the access token is omitted, a new one will be requested and the environment variable will be updated accordingly | + +In addition to the environment variables listed in the table above, there are a few environment variable that are specific to each test suite that you selected in "Step 1": + +| Connection Type | Environment variables | +|----------|-------------------| +| Api | *no additional environment variable necessary* | +| WebSockets | ZOOM_WEBSOCKET_SUBSCRIPTIONID | +| Chatbot | ZOOM_OAUTH_ACCOUNTID
ZOOM_CHATBOT_ROBOTJID (this is your Chatbot app's JID)
ZOOM_CHATBOT_TOJID (this is the JID of the user who will receive the messages sent during the integration tests) | + +Here's a convenient sample PowerShell script that demonstrates how to set some of the necessary environment variables: + +```powershell +[Environment]::SetEnvironmentVariable("ZOOM_OAUTH_CLIENTID", ".", "User") +[Environment]::SetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", "", "User") +``` From 9cf6777c900b35c53affa37cc95af0eea99bd1bd Mon Sep 17 00:00:00 2001 From: jericho Date: Sun, 6 Aug 2023 15:39:19 -0400 Subject: [PATCH 3/5] (GH-313) Test suites --- .../ZoomNet.IntegrationTests/ResultCodes.cs | 9 + Source/ZoomNet.IntegrationTests/TestSuite.cs | 117 ++++++++ .../TestSuites/ApiTestSuite.cs | 30 ++ .../TestSuites/ChatbotTestSuite.cs | 20 ++ .../TestSuites/WebSocketsTestSuite.cs | 50 +++ .../ZoomNet.IntegrationTests/Tests/Chatbot.cs | 19 +- .../ZoomNet.IntegrationTests/TestsRunner.cs | 284 ++++++------------ 7 files changed, 328 insertions(+), 201 deletions(-) create mode 100644 Source/ZoomNet.IntegrationTests/ResultCodes.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuite.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs create mode 100644 Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs diff --git a/Source/ZoomNet.IntegrationTests/ResultCodes.cs b/Source/ZoomNet.IntegrationTests/ResultCodes.cs new file mode 100644 index 00000000..77604b13 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/ResultCodes.cs @@ -0,0 +1,9 @@ +namespace ZoomNet.IntegrationTests +{ + internal enum ResultCodes + { + Success = 0, + Exception = 1, + Cancelled = 1223 + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuite.cs new file mode 100644 index 00000000..2d951c70 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuite.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace ZoomNet.IntegrationTests +{ + internal abstract class TestSuite + { + private const int MAX_ZOOM_API_CONCURRENCY = 5; + private const int TEST_NAME_MAX_LENGTH = 25; + private const string SUCCESSFUL_TEST_MESSAGE = "Completed successfully"; + + public ILoggerFactory LoggerFactory { get; init; } + + public IConnectionInfo ConnectionInfo { get; init; } + + public IWebProxy Proxy { get; init; } + + public Type[] Tests { get; init; } + + public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory, Type[] tests) + { + ConnectionInfo = connectionInfo; + Proxy = proxy; + LoggerFactory = loggerFactory; + Tests = tests; + } + + public virtual async Task RunTestsAsync() + { + // Configure cancellation + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + + // Configure ZoomNet client + var client = new ZoomClient(ConnectionInfo, Proxy, null, LoggerFactory.CreateLogger()); + + // Get my user and permisisons + var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); + var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); + Array.Sort(myPermissions); // Sort permissions alphabetically for convenience + + // Execute the async tests in parallel (with max degree of parallelism) + var results = await Tests.ForEachAsync( + async testType => + { + var log = new StringWriter(); + + try + { + var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); + await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); + } + catch (OperationCanceledException) + { + await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + } + catch (Exception e) + { + var exceptionMessage = e.GetBaseException().Message; + await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); + return (TestName: testType.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + } + finally + { + lock (Console.Out) + { + Console.Out.WriteLine(log.ToString()); + } + } + }, MAX_ZOOM_API_CONCURRENCY) + .ConfigureAwait(false); + + // Display summary + var summary = new StringWriter(); + await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + + var nameMaxLength = Math.Min(results.Max(r => r.TestName.Length), TEST_NAME_MAX_LENGTH); + foreach (var (TestName, ResultCode, Message) in results.OrderBy(r => r.TestName).ToArray()) + { + await summary.WriteLineAsync($"{TestName.ToExactLength(nameMaxLength)} : {Message}").ConfigureAwait(false); + } + + await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); + await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); + + // Prompt user to press a key in order to allow reading the log in the console + var promptLog = new StringWriter(); + await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); + await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); + ConsoleUtils.Prompt(promptLog.ToString()); + + // Return code indicating success/failure + var resultCode = ResultCodes.Success; + if (results.Any(result => result.ResultCode != ResultCodes.Success)) + { + if (results.Any(result => result.ResultCode == ResultCodes.Exception)) return ResultCodes.Exception; + else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = ResultCodes.Cancelled; + else resultCode = results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; + } + + return resultCode; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs new file mode 100644 index 00000000..54f7bef2 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using ZoomNet.IntegrationTests.Tests; + +namespace ZoomNet.IntegrationTests.TestSuites +{ + internal class ApiTestSuite : TestSuite + { + private static readonly Type[] _tests = new Type[] + { + typeof(Accounts), + typeof(CallLogs), + typeof(Chat), + typeof(CloudRecordings), + typeof(Contacts), + typeof(Dashboards), + typeof(Meetings), + typeof(Roles), + typeof(Users), + typeof(Webinars), + typeof(Reports), + }; + + public ApiTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : + base(connectionInfo, proxy, loggerFactory, _tests) + { + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs new file mode 100644 index 00000000..93210eb2 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using ZoomNet.IntegrationTests.Tests; + +namespace ZoomNet.IntegrationTests.TestSuites +{ + internal class ChatbotTestSuite : TestSuite + { + private static readonly Type[] _tests = new Type[] + { + typeof(Chatbot), + }; + + public ChatbotTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : + base(connectionInfo, proxy, loggerFactory, _tests) + { + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs new file mode 100644 index 00000000..4b9c9560 --- /dev/null +++ b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using ZoomNet.Models.Webhooks; + +namespace ZoomNet.IntegrationTests.TestSuites +{ + internal class WebSocketsTestSuite : TestSuite + { + private readonly string _subscriptionId; + + public WebSocketsTestSuite(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy, ILoggerFactory loggerFactory) : + base(connectionInfo, proxy, loggerFactory, Array.Empty()) + { + _subscriptionId = subscriptionId; + } + + public override async Task RunTestsAsync() + { + var logger = base.LoggerFactory.CreateLogger(); + var eventProcessor = new Func(async (webhookEvent, cancellationToken) => + { + if (!cancellationToken.IsCancellationRequested) + { + logger.LogInformation("Processing {eventType} event...", webhookEvent.EventType); + await Task.Delay(1, cancellationToken).ConfigureAwait(false); // This async call gets rid of "CS1998 This async method lacks 'await' operators and will run synchronously". + } + }); + + // Configure cancellation (this allows you to press CTRL+C or CTRL+Break to stop the websocket client) + var cts = new CancellationTokenSource(); + var exitEvent = new ManualResetEvent(false); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + exitEvent.Set(); + }; + + // Start the websocket client + using var client = new ZoomWebSocketClient(base.ConnectionInfo, _subscriptionId, eventProcessor, base.Proxy, logger); + await client.StartAsync(cts.Token).ConfigureAwait(false); + exitEvent.WaitOne(); + + return ResultCodes.Success; + } + } +} diff --git a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs index a2894f3d..f851048e 100644 --- a/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs +++ b/Source/ZoomNet.IntegrationTests/Tests/Chatbot.cs @@ -12,20 +12,22 @@ namespace ZoomNet.IntegrationTests.Tests; public class Chatbot : IIntegrationTest { /// - public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, - CancellationToken cancellationToken) + public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient client, TextWriter log, CancellationToken cancellationToken) { - var accountId = myUser?.AccountId ?? "{accountId}"; - var robotJId = "{robotId}@xmpp.zoom.us"; - var toJId = "{userId}@xmpp.zoom.us"; // User - //var toJId = "{channelId}@conference.xmpp.zoom.us"; // Channel + var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); + var robotJId = Environment.GetEnvironmentVariable("ZOOM_CHATBOT_ROBOTJID", EnvironmentVariableTarget.User); + var toJId = Environment.GetEnvironmentVariable("ZOOM_CHATBOT_TOJID", EnvironmentVariableTarget.User); + var response = await client.Chatbot.SendMessageAsync(accountId, toJId, robotJId, "Test message", false, cancellationToken); await log.WriteLineAsync(response.MessageId); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.EditMessageAsync(response.MessageId, accountId, toJId, robotJId, "*Updated test message*", true, cancellationToken); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, cancellationToken); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.SendMessageAsync(accountId, toJId, robotJId, new ChatbotContent() { @@ -126,6 +128,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie }, true, cancellationToken); await log.WriteLineAsync(response.MessageId); await Task.Delay(1000, cancellationToken); + response = await client.Chatbot.EditMessageAsync(response.MessageId, accountId, toJId, robotJId, new ChatbotContent() { @@ -228,7 +231,7 @@ public async Task RunAsync(User myUser, string[] myPermissions, IZoomClient clie } }, true, cancellationToken); await Task.Delay(1000, cancellationToken); - response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, - cancellationToken); + + response = await client.Chatbot.DeleteMessageAsync(response.MessageId, accountId, toJId, robotJId, cancellationToken); } } diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 72d283ca..0c945d20 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -1,38 +1,27 @@ using Microsoft.Extensions.Logging; using System; -using System.IO; -using System.Linq; using System.Net; -using System.Threading; using System.Threading.Tasks; -using ZoomNet.IntegrationTests.Tests; -using ZoomNet.Models.Webhooks; +using ZoomNet.IntegrationTests.TestSuites; namespace ZoomNet.IntegrationTests { internal class TestsRunner { - private const int MAX_ZOOM_API_CONCURRENCY = 5; - private const int TEST_NAME_MAX_LENGTH = 25; - private const string SUCCESSFUL_TEST_MESSAGE = "Completed successfully"; - - private enum ResultCodes - { - Success = 0, - Exception = 1, - Cancelled = 1223 - } - private enum TestType { - Api = 0, - WebSockets = 1, + Api, + WebSockets, + Chatbot, } private enum ConnectionType { - Jwt = 1, - OAuth = 2, + Jwt, // Zoom disabled the ability to create new JWT apps on June 1, 2023. The projected end-of-life for JWT apps is September 1, 2023. + OAuthAuthorizationCode, // Gets authorization code and sets refresh token. + OAuthRefreshToken, // Gets and sets refresh token and access token. + OAuthClientCredentials, // Gets and sets access token. For cleanliness, it should use a different access token environment variable so they don't cross contaminate. + OAuthServerToServer, // Gets the account id and access token and sets access token. Same as above. } private readonly ILoggerFactory _loggerFactory; @@ -42,216 +31,125 @@ public TestsRunner(ILoggerFactory loggerFactory) _loggerFactory = loggerFactory; } - public Task RunAsync() + public async Task RunAsync() { // ----------------------------------------------------------------------------- - // Do you want to proxy requests through Fiddler? Can be useful for debugging. - var useFiddler = true; - var fiddlerPort = 8888; // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 + // Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. + var useProxy = true; - // What tests do you want to run and which connection type do you want to use? + // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 + var proxyPort = 8888; + + // What tests do you want to run var testType = TestType.Api; - var connectionType = ConnectionType.OAuth; + + // Which connection type do you want to use? + var connectionType = ConnectionType.OAuthServerToServer; // ----------------------------------------------------------------------------- // Ensure the Console is tall enough and centered on the screen if (OperatingSystem.IsWindows()) Console.WindowHeight = Math.Min(60, Console.LargestWindowHeight); ConsoleUtils.CenterConsole(); - // Configure the proxy if desired (very useful for debugging) - var proxy = useFiddler ? new WebProxy($"http://localhost:{fiddlerPort}") : null; + // Configure the proxy if desired + var proxy = useProxy ? new WebProxy($"http://localhost:{proxyPort}") : null; - // Run tests either with a JWT or OAuth connection - return connectionType switch - { - ConnectionType.Jwt => RunTestsWithJwtConnectionAsync(testType, proxy), - ConnectionType.OAuth => RunTestsWithOAuthConnectionAsync(testType, proxy), - _ => throw new Exception("Unknwon connection type"), - }; - } - - private Task RunTestsWithJwtConnectionAsync(TestType testType, IWebProxy proxy) - { - if (testType != TestType.Api) throw new Exception("Only API tests are supported with JWT"); + // Get the connection info and test suite + var connectionInfo = GetConnectionInfo(connectionType); + var testSuite = GetTestSuite(connectionInfo, testType, proxy, _loggerFactory); - var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User); - var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User); - var connectionInfo = new JwtConnectionInfo(apiKey, apiSecret); + // Run the tests + var resultCode = await testSuite.RunTestsAsync().ConfigureAwait(false); - return RunApiTestsAsync(connectionInfo, proxy); + // Return result + return (int)resultCode; } - private Task RunTestsWithOAuthConnectionAsync(TestType testType, IWebProxy proxy) + private static IConnectionInfo GetConnectionInfo(ConnectionType connectionType) { - var clientId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTID", EnvironmentVariableTarget.User); - var clientSecret = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", EnvironmentVariableTarget.User); - var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); - var refreshToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", EnvironmentVariableTarget.User); - var subscriptionId = Environment.GetEnvironmentVariable("ZOOM_WEBSOCKET_SUBSCRIPTIONID", EnvironmentVariableTarget.User); - - IConnectionInfo connectionInfo; - - // Server-to-Server OAuth - if (!string.IsNullOrEmpty(accountId)) + // Jwt + if (connectionType == ConnectionType.Jwt) { - connectionInfo = OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId); - } - - // Standard OAuth - else - { - connectionInfo = OAuthConnectionInfo.WithRefreshToken(clientId, clientSecret, refreshToken, - (newRefreshToken, newAccessToken) => - { - Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); - }); - - //var authorizationCode = "<-- the code generated by Zoom when the app is authorized by the user -->"; - //connectionInfo = OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode, - // (newRefreshToken, newAccessToken) => - // { - // Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); - // }); + var apiKey = Environment.GetEnvironmentVariable("ZOOM_JWT_APIKEY", EnvironmentVariableTarget.User); + var apiSecret = Environment.GetEnvironmentVariable("ZOOM_JWT_APISECRET", EnvironmentVariableTarget.User); + return new JwtConnectionInfo(apiKey, apiSecret); } - // Execute either the API or Websocket tests - return testType switch - { - TestType.Api => RunApiTestsAsync(connectionInfo, proxy), - TestType.WebSockets => RunWebSocketTestsAsync(connectionInfo, subscriptionId, proxy), - _ => throw new Exception("Unknwon test type"), - }; - } - - private async Task RunApiTestsAsync(IConnectionInfo connectionInfo, IWebProxy proxy) - { - // Configure cancellation - var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - cts.Cancel(); - }; + // OAuth + var clientId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTID", EnvironmentVariableTarget.User); + var clientSecret = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", EnvironmentVariableTarget.User); - // Configure ZoomNet client - var client = new ZoomClient(connectionInfo, proxy, null, _loggerFactory.CreateLogger()); + if (string.IsNullOrEmpty(clientId)) throw new Exception("You must set the ZOOM_OAUTH_CLIENTID environment variable before you can run integration tests."); + if (string.IsNullOrEmpty(clientSecret)) throw new Exception("You must set the ZOOM_OAUTH_CLIENTSECRET environment variable before you can run integration tests."); - // These are the integration tests that we will execute - var integrationTests = new Type[] + switch (connectionType) { - typeof(Accounts), - typeof(CallLogs), - typeof(Chat), - typeof(CloudRecordings), - typeof(Contacts), - typeof(Dashboards), - typeof(Meetings), - typeof(Roles), - typeof(Users), - typeof(Webinars), - typeof(Reports), - typeof(Chatbot) - }; - - // Get my user and permisisons - var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); - var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); - Array.Sort(myPermissions); // Sort permissions alphabetically for convenience + case ConnectionType.OAuthAuthorizationCode: + { + var authorizationCode = Environment.GetEnvironmentVariable("ZOOM_OAUTH_AUTHORIZATIONCODE", EnvironmentVariableTarget.User); - // Execute the async tests in parallel (with max degree of parallelism) - var results = await integrationTests.ForEachAsync( - async testType => - { - var log = new StringWriter(); + if (string.IsNullOrEmpty(authorizationCode)) throw new Exception("Either the autorization code environment variable has not been set or it's no longer available because you already used it once."); - try + return OAuthConnectionInfo.WithAuthorizationCode(clientId, clientSecret, authorizationCode, + (newRefreshToken, newAccessToken) => + { + // Clear the authorization code because it's intended to be used only once + Environment.SetEnvironmentVariable("ZOOM_OAUTH_AUTHORIZATIONCODE", string.Empty, EnvironmentVariableTarget.User); + Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + }); + } + case ConnectionType.OAuthRefreshToken: { - var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); - await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false); - return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); + var refreshToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", EnvironmentVariableTarget.User); + if (string.IsNullOrEmpty(refreshToken)) throw new Exception("You must set the ZOOM_OAUTH_REFRESHTOKEN environment variable before you can run integration tests."); + + return OAuthConnectionInfo.WithRefreshToken(clientId, clientSecret, refreshToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_REFRESHTOKEN", newRefreshToken, EnvironmentVariableTarget.User); + }); } - catch (OperationCanceledException) + case ConnectionType.OAuthClientCredentials: { - await log.WriteLineAsync($"-----> TASK CANCELLED").ConfigureAwait(false); - return (TestName: testType.Name, ResultCode: ResultCodes.Cancelled, Message: "Task cancelled"); + var accessToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN", EnvironmentVariableTarget.User); + + return OAuthConnectionInfo.WithClientCredentials(clientId, clientSecret, accessToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + }); } - catch (Exception e) + case ConnectionType.OAuthServerToServer: { - var exceptionMessage = e.GetBaseException().Message; - await log.WriteLineAsync($"-----> AN EXCEPTION OCCURRED: {exceptionMessage}").ConfigureAwait(false); - return (TestName: testType.Name, ResultCode: ResultCodes.Exception, Message: exceptionMessage); + var accountId = Environment.GetEnvironmentVariable("ZOOM_OAUTH_ACCOUNTID", EnvironmentVariableTarget.User); + var accessToken = Environment.GetEnvironmentVariable("ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN", EnvironmentVariableTarget.User); + + return OAuthConnectionInfo.ForServerToServer(clientId, clientSecret, accountId, accessToken, + (newRefreshToken, newAccessToken) => + { + Environment.SetEnvironmentVariable("ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN", newAccessToken, EnvironmentVariableTarget.User); + }); } - finally + default: { - lock (Console.Out) - { - Console.Out.WriteLine(log.ToString()); - } + throw new Exception("Unknwon connection type"); } - }, MAX_ZOOM_API_CONCURRENCY) - .ConfigureAwait(false); - - // Display summary - var summary = new StringWriter(); - await summary.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await summary.WriteLineAsync("******************** SUMMARY *********************").ConfigureAwait(false); - await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); - - var nameMaxLength = Math.Min(results.Max(r => r.TestName.Length), TEST_NAME_MAX_LENGTH); - foreach (var (TestName, ResultCode, Message) in results.OrderBy(r => r.TestName).ToArray()) - { - await summary.WriteLineAsync($"{TestName.ToExactLength(nameMaxLength)} : {Message}").ConfigureAwait(false); - } - - await summary.WriteLineAsync("**************************************************").ConfigureAwait(false); - await Console.Out.WriteLineAsync(summary.ToString()).ConfigureAwait(false); - - // Prompt user to press a key in order to allow reading the log in the console - var promptLog = new StringWriter(); - await promptLog.WriteLineAsync("\n\n**************************************************").ConfigureAwait(false); - await promptLog.WriteLineAsync("Press any key to exit").ConfigureAwait(false); - ConsoleUtils.Prompt(promptLog.ToString()); - - // Return code indicating success/failure - var resultCode = (int)ResultCodes.Success; - if (results.Any(result => result.ResultCode != ResultCodes.Success)) - { - if (results.Any(result => result.ResultCode == ResultCodes.Exception)) resultCode = (int)ResultCodes.Exception; - else if (results.Any(result => result.ResultCode == ResultCodes.Cancelled)) resultCode = (int)ResultCodes.Cancelled; - else resultCode = (int)results.First(result => result.ResultCode != ResultCodes.Success).ResultCode; - } - - return resultCode; + }; } - private async Task RunWebSocketTestsAsync(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy) + private static TestSuite GetTestSuite(IConnectionInfo connectionInfo, TestType testType, IWebProxy proxy, ILoggerFactory loggerFactory) { - var logger = _loggerFactory.CreateLogger(); - var eventProcessor = new Func(async (webhookEvent, cancellationToken) => + switch (testType) { - if (!cancellationToken.IsCancellationRequested) - { - logger.LogInformation("Processing {eventType} event...", webhookEvent.EventType); - await Task.Delay(1, cancellationToken).ConfigureAwait(false); // This async call gets rid of "CS1998 This async method lacks 'await' operators and will run synchronously". - } - }); - - // Configure cancellation (this allows you to press CTRL+C or CTRL+Break to stop the websocket client) - var cts = new CancellationTokenSource(); - var exitEvent = new ManualResetEvent(false); - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - cts.Cancel(); - exitEvent.Set(); + case TestType.Api: return new ApiTestSuite(connectionInfo, proxy, loggerFactory); + case TestType.Chatbot: return new ChatbotTestSuite(connectionInfo, proxy, loggerFactory); + case TestType.WebSockets: + { + var subscriptionId = Environment.GetEnvironmentVariable("ZOOM_WEBSOCKET_SUBSCRIPTIONID", EnvironmentVariableTarget.User); + return new WebSocketsTestSuite(connectionInfo, subscriptionId, proxy, loggerFactory); + } + default: throw new Exception("Unknwon test type"); }; - - // Start the websocket client - using var client = new ZoomWebSocketClient(connectionInfo, subscriptionId, eventProcessor, proxy, logger); - await client.StartAsync(cts.Token).ConfigureAwait(false); - exitEvent.WaitOne(); - - return (int)ResultCodes.Success; } } } From c4c70daa64c83bbf3de909fd74404b91a9d6e4df Mon Sep 17 00:00:00 2001 From: jericho Date: Mon, 7 Aug 2023 10:46:55 -0400 Subject: [PATCH 4/5] (GH-313) Add boolean to indicate if we want the current user to be fetched prior to running integration tests. This is necessary because in some scenarios (such as when running Chatbot tests) we do not have access to Zoom's REST API --- Source/ZoomNet.IntegrationTests/TestSuite.cs | 20 ++++++++++++++----- .../TestSuites/ApiTestSuite.cs | 2 +- .../TestSuites/ChatbotTestSuite.cs | 2 +- .../TestSuites/WebSocketsTestSuite.cs | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Source/ZoomNet.IntegrationTests/TestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuite.cs index 2d951c70..eaaafd3b 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuite.cs @@ -5,6 +5,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using ZoomNet.Models; namespace ZoomNet.IntegrationTests { @@ -22,12 +23,15 @@ internal abstract class TestSuite public Type[] Tests { get; init; } - public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory, Type[] tests) + public bool FetchCurrentUserInfo { get; init; } + + public TestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory, Type[] tests, bool fetchCurrentUserInfo) { ConnectionInfo = connectionInfo; Proxy = proxy; LoggerFactory = loggerFactory; Tests = tests; + FetchCurrentUserInfo = fetchCurrentUserInfo; } public virtual async Task RunTestsAsync() @@ -44,9 +48,15 @@ public virtual async Task RunTestsAsync() var client = new ZoomClient(ConnectionInfo, Proxy, null, LoggerFactory.CreateLogger()); // Get my user and permisisons - var myUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); - var myPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); - Array.Sort(myPermissions); // Sort permissions alphabetically for convenience + User currentUser = null; + string[] currentUserPermissions = Array.Empty(); + + if (FetchCurrentUserInfo) + { + currentUser = await client.Users.GetCurrentAsync(cts.Token).ConfigureAwait(false); + currentUserPermissions = await client.Users.GetCurrentPermissionsAsync(cts.Token).ConfigureAwait(false); + Array.Sort(currentUserPermissions); // Sort permissions alphabetically for convenience + } // Execute the async tests in parallel (with max degree of parallelism) var results = await Tests.ForEachAsync( @@ -57,7 +67,7 @@ public virtual async Task RunTestsAsync() try { var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); - await integrationTest.RunAsync(myUser, myPermissions, client, log, cts.Token).ConfigureAwait(false); + await integrationTest.RunAsync(currentUser, currentUserPermissions, client, log, cts.Token).ConfigureAwait(false); return (TestName: testType.Name, ResultCode: ResultCodes.Success, Message: SUCCESSFUL_TEST_MESSAGE); } catch (OperationCanceledException) diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs index 54f7bef2..26829f35 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs @@ -23,7 +23,7 @@ internal class ApiTestSuite : TestSuite }; public ApiTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : - base(connectionInfo, proxy, loggerFactory, _tests) + base(connectionInfo, proxy, loggerFactory, _tests, true) { } } diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs index 93210eb2..1b95fa18 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs @@ -13,7 +13,7 @@ internal class ChatbotTestSuite : TestSuite }; public ChatbotTestSuite(IConnectionInfo connectionInfo, IWebProxy proxy, ILoggerFactory loggerFactory) : - base(connectionInfo, proxy, loggerFactory, _tests) + base(connectionInfo, proxy, loggerFactory, _tests, false) { } } diff --git a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs index 4b9c9560..9146773f 100644 --- a/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs +++ b/Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs @@ -12,7 +12,7 @@ internal class WebSocketsTestSuite : TestSuite private readonly string _subscriptionId; public WebSocketsTestSuite(IConnectionInfo connectionInfo, string subscriptionId, IWebProxy proxy, ILoggerFactory loggerFactory) : - base(connectionInfo, proxy, loggerFactory, Array.Empty()) + base(connectionInfo, proxy, loggerFactory, Array.Empty(), false) { _subscriptionId = subscriptionId; } From d9ade4b4150b9657e880b80f7e5c305869675c9f Mon Sep 17 00:00:00 2001 From: jericho Date: Thu, 17 Aug 2023 09:44:41 -0400 Subject: [PATCH 5/5] Fix typo in comment --- Source/ZoomNet.IntegrationTests/TestsRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ZoomNet.IntegrationTests/TestsRunner.cs b/Source/ZoomNet.IntegrationTests/TestsRunner.cs index 0c945d20..e4f2d2c2 100644 --- a/Source/ZoomNet.IntegrationTests/TestsRunner.cs +++ b/Source/ZoomNet.IntegrationTests/TestsRunner.cs @@ -37,7 +37,7 @@ public async Task RunAsync() // Do you want to proxy requests through a tool such as Fiddler? Very useful for debugging. var useProxy = true; - // By default Fiddler4 uses port 8888 and Fiddler Everywhere uses port 8866 + // By default Fiddler Classic uses port 8888 and Fiddler Everywhere uses port 8866 var proxyPort = 8888; // What tests do you want to run