Skip to content

Commit

Permalink
Merge branch 'release/0.67.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Sep 22, 2023
2 parents bccc190 + 34f3706 commit 2a986c6
Show file tree
Hide file tree
Showing 62 changed files with 2,825 additions and 266 deletions.
82 changes: 82 additions & 0 deletions Source/ZoomNet.IntegrationTests/README.md
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")
```
9 changes: 9 additions & 0 deletions Source/ZoomNet.IntegrationTests/ResultCodes.cs
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
}
}
127 changes: 127 additions & 0 deletions Source/ZoomNet.IntegrationTests/TestSuite.cs
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 Source/ZoomNet.IntegrationTests/TestSuites/ApiTestSuite.cs
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 Source/ZoomNet.IntegrationTests/TestSuites/ChatbotTestSuite.cs
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 Source/ZoomNet.IntegrationTests/TestSuites/WebSocketsTestSuite.cs
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;
}
}
}
Loading

0 comments on commit 2a986c6

Please sign in to comment.