-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
62 changed files
with
2,825 additions
and
266 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<br/>ZOOM_JWT_APISECRET | | ||
| OAuthAuthorizationCode | ZOOM_OAUTH_CLIENTID<br/>ZOOM_OAUTH_CLIENTSECRET<br/>ZOOM_OAUTH_AUTHORIZATIONCODE<br/>**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<br/>ZOOM_OAUTH_CLIENTSECRET<br/>ZOOM_OAUTH_REFRESHTOKEN<br/>**Note** The refresh token is initially created when an authorization code is used | | ||
| OAuthClientCredentials | ZOOM_OAUTH_CLIENTID<br/>ZOOM_OAUTH_CLIENTSECRET<br/>ZOOM_OAUTH_CLIENTCREDENTIALS_ACCESSTOKEN *(optional)*<br/>**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<br/>ZOOM_OAUTH_CLIENTSECRET<br/>ZOOM_OAUTH_ACCOUNTID<br/>ZOOM_OAUTH_SERVERTOSERVER_ACCESSTOKEN *(optional)*<br/>**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<br/>ZOOM_CHATBOT_ROBOTJID (this is your Chatbot app's JID)<br/>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", "<insert your client id>.", "User") | ||
[Environment]::SetEnvironmentVariable("ZOOM_OAUTH_CLIENTSECRET", "<insert your client secret>", "User") | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace ZoomNet.IntegrationTests | ||
{ | ||
internal enum ResultCodes | ||
{ | ||
Success = 0, | ||
Exception = 1, | ||
Cancelled = 1223 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
using Microsoft.Extensions.Logging; | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using ZoomNet.Models; | ||
|
||
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 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<ResultCodes> 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<ZoomClient>()); | ||
|
||
// Get my user and permisisons | ||
User currentUser = null; | ||
string[] currentUserPermissions = Array.Empty<string>(); | ||
|
||
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( | ||
async testType => | ||
{ | ||
var log = new StringWriter(); | ||
|
||
try | ||
{ | ||
var integrationTest = (IIntegrationTest)Activator.CreateInstance(testType); | ||
await integrationTest.RunAsync(currentUser, currentUserPermissions, 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; | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, true) | ||
{ | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, false) | ||
{ | ||
} | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Type>(), false) | ||
{ | ||
_subscriptionId = subscriptionId; | ||
} | ||
|
||
public override async Task<ResultCodes> RunTestsAsync() | ||
{ | ||
var logger = base.LoggerFactory.CreateLogger<ZoomWebSocketClient>(); | ||
var eventProcessor = new Func<Event, CancellationToken, Task>(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; | ||
} | ||
} | ||
} |
Oops, something went wrong.