• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Jericho / ZoomNet / 894

07 Mar 2025 07:00PM UTC coverage: 20.268% (-0.03%) from 20.298%
894

push

appveyor

Jericho
Fix "Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive."

666 of 3286 relevant lines covered (20.27%)

11.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.89
/Source/ZoomNet/ZoomClient.cs
1
using Microsoft.Extensions.Logging;
2
using Microsoft.Extensions.Logging.Abstractions;
3
using Pathoschild.Http.Client;
4
using Pathoschild.Http.Client.Extensibility;
5
using System;
6
using System.Collections.Generic;
7
using System.Linq;
8
using System.Net;
9
using System.Net.Http;
10
using System.Reflection;
11
using ZoomNet.Json;
12
using ZoomNet.Resources;
13
using ZoomNet.Utilities;
14

15
namespace ZoomNet
16
{
17
        /// <summary>
18
        /// REST client for interacting with Zoom's API.
19
        /// </summary>
20
        /// <remarks>
21
        /// Don't be fooled by the fact that this class implements the IDisposable interface: it is not meant to be short-lived and instantiated with every request.
22
        /// It is meant to be long-lived and re-used throughout the life of an application.
23
        /// The reason is: we use Microsoft's HttpClient to dispatch requests which itself is meant to be long-lived and re-used.
24
        /// Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads and will result in SocketException errors.
25
        ///
26
        /// See <a href="https://github.com/Jericho/ZoomNet/issues/35">this discussion</a> for more information about managing the lifetime of your client instance.
27
        /// </remarks>
28
        public class ZoomClient : IZoomClient, IDisposable
29
        {
30
                #region FIELDS
31

32
                private const string ZOOM_V2_BASE_URI = "https://api.zoom.us/v2";
33

34
                private static string _version;
35

36
                private readonly bool _mustDisposeHttpClient;
37
                private readonly ZoomClientOptions _options;
38
                private readonly ILogger _logger;
39

40
                private HttpClient _httpClient;
41
                private IClient _fluentClient;
42

43
                #endregion
44

45
                #region PROPERTIES
46

47
                /// <summary>
48
                /// Gets the Version.
49
                /// </summary>
50
                /// <value>
51
                /// The version.
52
                /// </value>
53
                public static string Version
54
                {
55
                        get
56
                        {
57
                                if (string.IsNullOrEmpty(_version))
4✔
58
                                {
59
                                        _version = typeof(ZoomClient).GetTypeInfo().Assembly.GetName().Version.ToString(3);
1✔
60
#if DEBUG
61
                                        _version = "DEBUG";
62
#endif
63
                                }
64

65
                                return _version;
4✔
66
                        }
67
                }
68

69
                /// <inheritdoc/>
70
                public IAccounts Accounts { get; private set; }
71

72
                /// <inheritdoc/>
73
                public ICallLogs CallLogs { get; private set; }
74

75
                /// <inheritdoc/>
76
                public IChat Chat { get; private set; }
77

78
                /// <inheritdoc/>
79
                public IChatbot Chatbot { get; private set; }
80

81
                /// <inheritdoc/>
82
                public ICloudRecordings CloudRecordings { get; private set; }
83

84
                /// <inheritdoc/>
85
                public IContacts Contacts { get; private set; }
86

87
                /// <inheritdoc/>
88
                public IDashboards Dashboards { get; private set; }
89

90
                /// <inheritdoc/>
91
                [Obsolete("The Data Compliance API is deprecated")]
92
                public IDataCompliance DataCompliance { get; private set; }
93

94
                /// <inheritdoc/>
95
                public IExternalContacts ExternalContacts { get; private set; }
96

97
                /// <inheritdoc/>
98
                public IGroups Groups { get; private set; }
99

100
                /// <inheritdoc/>
101
                public IMeetings Meetings { get; private set; }
102

103
                /// <inheritdoc/>
104
                public IPastMeetings PastMeetings { get; private set; }
105

106
                /// <inheritdoc/>
107
                public IPastWebinars PastWebinars { get; private set; }
108

109
                /// <inheritdoc/>
110
                public IPhone Phone { get; private set; }
111

112
                /// <inheritdoc/>
113
                public IReports Reports { get; private set; }
114

115
                /// <inheritdoc/>
116
                public IRoles Roles { get; private set; }
117

118
                /// <inheritdoc/>
119
                public ISms Sms { get; private set; }
120

121
                /// <inheritdoc/>
122
                public IUsers Users { get; private set; }
123

124
                /// <inheritdoc/>
125
                public IWebinars Webinars { get; private set; }
126

127
                #endregion
128

129
                #region CTOR
130

131
                /// <summary>
132
                /// Initializes a new instance of the <see cref="ZoomClient"/> class.
133
                /// </summary>
134
                /// <param name="connectionInfo">Connection information.</param>
135
                /// <param name="options">Options for the Zoom client.</param>
136
                /// <param name="logger">Logger.</param>
137
                public ZoomClient(IConnectionInfo connectionInfo, ZoomClientOptions options = null, ILogger logger = null)
138
                        : this(connectionInfo, new HttpClient(), true, options, logger)
1✔
139
                {
140
                }
×
141

142
                /// <summary>
143
                /// Initializes a new instance of the <see cref="ZoomClient"/> class with a specific proxy.
144
                /// </summary>
145
                /// <param name="connectionInfo">Connection information.</param>
146
                /// <param name="proxy">Allows you to specify a proxy.</param>
147
                /// <param name="options">Options for the Zoom client.</param>
148
                /// <param name="logger">Logger.</param>
149
                public ZoomClient(IConnectionInfo connectionInfo, IWebProxy proxy, ZoomClientOptions options = null, ILogger logger = null)
150
                        : this(connectionInfo, new HttpClient(new HttpClientHandler { Proxy = proxy, UseProxy = proxy != null }), true, options, logger)
1✔
151
                {
152
                }
1✔
153

154
                /// <summary>
155
                /// Initializes a new instance of the <see cref="ZoomClient"/> class with a specific handler.
156
                /// </summary>
157
                /// <param name="connectionInfo">Connection information.</param>
158
                /// <param name="handler">TThe HTTP handler stack to use for sending requests.</param>
159
                /// <param name="options">Options for the Zoom client.</param>
160
                /// <param name="logger">Logger.</param>
161
                public ZoomClient(IConnectionInfo connectionInfo, HttpMessageHandler handler, ZoomClientOptions options = null, ILogger logger = null)
162
                        : this(connectionInfo, new HttpClient(handler), true, options, logger)
×
163
                {
164
                }
×
165

166
                /// <summary>
167
                /// Initializes a new instance of the <see cref="ZoomClient"/> class with a specific http client.
168
                /// </summary>
169
                /// <param name="connectionInfo">Connection information.</param>
170
                /// <param name="httpClient">Allows you to inject your own HttpClient. This is useful, for example, to setup the HtppClient with a proxy.</param>
171
                /// <param name="options">Options for the Zoom client.</param>
172
                /// <param name="logger">Logger.</param>
173
                public ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, ZoomClientOptions options = null, ILogger logger = null)
174
                        : this(connectionInfo, httpClient, false, options, logger)
1✔
175
                {
176
                }
1✔
177

178
                private ZoomClient(IConnectionInfo connectionInfo, HttpClient httpClient, bool disposeClient, ZoomClientOptions options, ILogger logger = null)
179
                {
180
                        if (connectionInfo == null) throw new ArgumentNullException(nameof(connectionInfo));
3✔
181

182
                        _mustDisposeHttpClient = disposeClient;
3✔
183
                        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
3✔
184
                        _options = options ?? new();
3✔
185
                        _logger = logger ?? NullLogger.Instance;
3✔
186
                        _fluentClient = new FluentClient(new Uri(ZOOM_V2_BASE_URI), httpClient)
3✔
187
                                .SetUserAgent($"ZoomNet/{Version} (+https://github.com/Jericho/ZoomNet)");
3✔
188

189
                        _fluentClient.Filters.Remove<DefaultErrorFilter>();
3✔
190

191
                        // Remove all the built-in formatters and replace them with our custom JSON formatter
192
                        _fluentClient.Formatters.Clear();
3✔
193
                        _fluentClient.Formatters.Add(new JsonFormatter());
3✔
194

195
                        // Order is important: the token handler (either JWT or OAuth) must be first, followed by DiagnosticHandler and then by ErrorHandler.
196
                        if (connectionInfo is JwtConnectionInfo jwtConnectionInfo)
3✔
197
                        {
198
                                var tokenHandler = new JwtTokenHandler(jwtConnectionInfo);
2✔
199
                                _fluentClient.Filters.Add(tokenHandler);
1✔
200
                                _fluentClient.SetRequestCoordinator(new ZoomRetryCoordinator(new Http429RetryStrategy(), tokenHandler));
1✔
201
                        }
202
                        else if (connectionInfo is OAuthConnectionInfo oAuthConnectionInfo)
1✔
203
                        {
204
                                var tokenHandler = new OAuthTokenHandler(oAuthConnectionInfo, httpClient);
1✔
205
                                _fluentClient.Filters.Add(tokenHandler);
1✔
206
                                _fluentClient.SetRequestCoordinator(new ZoomRetryCoordinator(new Http429RetryStrategy(), tokenHandler));
1✔
207
                        }
208
                        else
209
                        {
210
                                throw new ZoomException($"{connectionInfo.GetType()} is an unknown connection type", null, null, null, null);
×
211
                        }
212

213
                        // The list of filters must be kept in sync with the filters in Utils.GetFluentClient in the unit testing project.
214
                        _fluentClient.Filters.Add(new DiagnosticHandler(_options.LogLevelSuccessfulCalls, _options.LogLevelFailedCalls, _logger));
2✔
215
                        _fluentClient.Filters.Add(new ZoomErrorHandler());
2✔
216

217
                        Accounts = new Accounts(_fluentClient);
2✔
218
                        CallLogs = new CallLogs(_fluentClient);
2✔
219
                        Chat = new Chat(_fluentClient);
2✔
220
                        Chatbot = new Chatbot(_fluentClient);
2✔
221
                        CloudRecordings = new CloudRecordings(_fluentClient);
2✔
222
                        Contacts = new Contacts(_fluentClient);
2✔
223
                        Dashboards = new Dashboards(_fluentClient);
2✔
224
                        DataCompliance = new DataCompliance(_fluentClient);
2✔
225
                        ExternalContacts = new ExternalContacts(_fluentClient);
2✔
226
                        Groups = new Groups(_fluentClient);
2✔
227
                        Meetings = new Meetings(_fluentClient);
2✔
228
                        PastMeetings = new PastMeetings(_fluentClient);
2✔
229
                        PastWebinars = new PastWebinars(_fluentClient);
2✔
230
                        Phone = new Phone(_fluentClient);
2✔
231
                        Reports = new Reports(_fluentClient);
2✔
232
                        Roles = new Roles(_fluentClient);
2✔
233
                        Sms = new Sms(_fluentClient);
2✔
234
                        Users = new Users(_fluentClient);
2✔
235
                        Webinars = new Webinars(_fluentClient);
2✔
236
                }
2✔
237

238
                /// <summary>
239
                /// Finalizes an instance of the <see cref="ZoomClient"/> class.
240
                /// </summary>
241
                ~ZoomClient()
242
                {
243
                        // The object went out of scope and finalized is called.
244
                        // Call 'Dispose' to release unmanaged resources
245
                        // Managed resources will be released when GC runs the next time.
246
                        Dispose(false);
×
247
                }
×
248

249
                #endregion
250

251
                #region PUBLIC METHODS
252

253
                /// <inheritdoc/>
254
                public bool HasPermissions(IEnumerable<string> scopes)
255
                {
256
                        var tokenHandler = _fluentClient.Filters.OfType<OAuthTokenHandler>().SingleOrDefault();
×
257
                        if (tokenHandler == null) throw new Exception("The concept of scopes only applies when using an OAuth connection.");
×
258

259
                        // Ensure the token (and by extension the scopes) is not expired
260
                        tokenHandler.RefreshTokenIfNecessary(false);
×
261

262
                        // The list of scopes can be empty if a previously issued token was specified when the OAuthConnectionInfo was instantiated.
263
                        // I am not aware of any way to fetch the list of scopes which would enable me to populate the list of scopes in the OAuthConnectionInfo.
264
                        // Therefore in this scenario the only workaround I can think of is to force the token to be refreshed.
265
                        var oAuthConnectionInfo = (OAuthConnectionInfo)tokenHandler.ConnectionInfo;
×
266
                        if (oAuthConnectionInfo.Scopes == null) tokenHandler.RefreshTokenIfNecessary(true); // Force the token to be refreshed wich will have the side-effect of populating the '.Scopes'
×
267

268
                        var missingScopes = scopes.Except(((OAuthConnectionInfo)tokenHandler.ConnectionInfo).Scopes).ToArray();
×
269
                        return !missingScopes.Any();
×
270
                }
271

272
                /// <inheritdoc/>
273
                public void Dispose()
274
                {
275
                        // Call 'Dispose' to release resources
276
                        Dispose(true);
1✔
277

278
                        // Tell the GC that we have done the cleanup and there is nothing left for the Finalizer to do
279
                        GC.SuppressFinalize(this);
1✔
280
                }
1✔
281

282
                /// <summary>
283
                /// Releases unmanaged and - optionally - managed resources.
284
                /// </summary>
285
                /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
286
                protected virtual void Dispose(bool disposing)
287
                {
288
                        if (disposing)
1✔
289
                        {
290
                                ReleaseManagedResources();
1✔
291
                        }
292
                        else
293
                        {
294
                                // The object went out of scope and the Finalizer has been called.
295
                                // The GC will take care of releasing managed resources, therefore there is nothing to do here.
296
                        }
297

298
                        ReleaseUnmanagedResources();
1✔
299
                }
1✔
300

301
                #endregion
302

303
                #region PRIVATE METHODS
304

305
                private void ReleaseManagedResources()
306
                {
307
                        if (_fluentClient != null)
1✔
308
                        {
309
                                _fluentClient.Dispose();
1✔
310
                                _fluentClient = null;
1✔
311
                        }
312

313
                        if (_httpClient != null && _mustDisposeHttpClient)
1✔
314
                        {
315
                                _httpClient.Dispose();
1✔
316
                                _httpClient = null;
1✔
317
                        }
318
                }
1✔
319

320
                private void ReleaseUnmanagedResources()
321
                {
322
                        // We do not hold references to unmanaged resources
323
                }
1✔
324

325
                #endregion
326
        }
327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc