/// <summary> /// Equivalent to GetSetOfUniqueNodesAsync /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="endpointId">Id of the endpoint as returned by <see cref="Registry_GetEndpoints(IIoTPlatformTestContext)"/></param> /// <param name="nodeId">Id of the parent node or null to browse the root node</param> /// <param name="ct">Cancellation token</param> public static async Task <List <(string NodeId, string NodeClass, bool Children)> > GetBrowseEndpointAsync( IIoTPlatformTestContext context, string endpointId, string nodeId = null, CancellationToken ct = default) { if (string.IsNullOrEmpty(endpointId)) { context.OutputHelper.WriteLine($"{nameof(endpointId)} is null or empty"); throw new ArgumentNullException(nameof(endpointId)); } var result = new List <(string NodeId, string NodeClass, bool Children)>(); string continuationToken = null; do { var browseResult = await GetBrowseEndpoint_InternalAsync(context, endpointId, nodeId, continuationToken, ct); if (browseResult.results.Count > 0) { result.AddRange(browseResult.results); } continuationToken = browseResult.continuationToken; } while (continuationToken != null); return(result); }
/// <summary> /// Delete a file on the Edge VM /// </summary> /// <param name="fileName">Filename of the file to delete</param> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> public static void DeleteFileOnEdgeVM(string fileName, IIoTPlatformTestContext context) { var isSuccessful = false; using var client = CreateSshClientAndConnect(context); var terminal = client.RunCommand("rm " + fileName); if (string.IsNullOrEmpty(terminal.Error) || terminal.Error.ToLowerInvariant().Contains("no such file")) { isSuccessful = true; } Assert.True(isSuccessful, "Delete file was not successful"); if (context.IoTEdgeConfig.NestedEdgeFlag == "Enable") { using var sshCient = CreateSshClientAndConnect(context); foreach (var edge in context.IoTEdgeConfig.NestedEdgeSshConnections) { if (edge != string.Empty) { var command = $"ssh -oStrictHostKeyChecking=no {edge} 'sudo rm {fileName}'"; sshCient.RunCommand(command); } } } }
/// <summary> /// Gets endpoints from registry /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> private static async Task <dynamic> GetEndpointInternalAsync(IIoTPlatformTestContext context, CancellationToken ct) { var accessToken = await GetTokenAsync(context, ct).ConfigureAwait(false); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.GET); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = TestConstants.APIRoutes.RegistryEndpoints; var response = await client.ExecuteAsync(request, ct).ConfigureAwait(false); Assert.NotNull(response); if (!response.IsSuccessful) { context.OutputHelper?.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper?.WriteLine($"ErrorMessage: {response.ErrorMessage}"); Assert.True(response.IsSuccessful, "GET /registry/v2/endpoints failed!"); } return(JsonConvert.DeserializeObject <ExpandoObject>(response.Content, new ExpandoObjectConverter())); }
/// <summary> /// Unregisters a server identified by <paramref name="applicationId"/> /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="discoveryUrl">Discovery URL the application is registered to</param> /// <param name="ct">Cancellation token</param> public static async Task UnregisterServerAsync( IIoTPlatformTestContext context, string discoveryUrl, CancellationToken ct = default) { var applicationId = await GetApplicationIdAsync(context, discoveryUrl, ct); var accessToken = await GetTokenAsync(context, ct).ConfigureAwait(false); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.DELETE); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = string.Format(TestConstants.APIRoutes.RegistryApplicationsWithApplicationIdFormat, applicationId); var response = await client.ExecuteAsync(request, ct).ConfigureAwait(false); Assert.NotNull(response); if (!response.IsSuccessful) { context.OutputHelper?.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper?.WriteLine($"ErrorMessage: {response.ErrorMessage}"); Assert.True(response.IsSuccessful, "DELETE /registry/v2/application/{applicationId} failed!"); } }
/// <summary> /// Stops the monitoring of incoming event to an IoT Hub and returns success/failure. /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <returns></returns> public static async Task <dynamic> StopMonitoringIncomingMessagesAsync( IIoTPlatformTestContext context, CancellationToken ct = default ) { // TODO Merge with Start-Method to avoid code duplication var runtimeUrl = context.TestEventProcessorConfig.TestEventProcessorBaseUrl.TrimEnd('/') + "/Runtime"; var client = new RestClient(runtimeUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds, Authenticator = new HttpBasicAuthenticator(context.TestEventProcessorConfig.TestEventProcessorUsername, context.TestEventProcessorConfig.TestEventProcessorPassword) }; var body = new { CommandType = CommandEnum.Stop, }; var request = new RestRequest(Method.PUT); request.AddJsonBody(body); var response = await client.ExecuteAsync(request, ct); dynamic json = JsonConvert.DeserializeObject(response.Content); Assert.NotNull(json); Assert.NotEmpty(json); return(json); }
/// <summary> /// transfer the content of published_nodes.json file into the OPC Publisher edge module /// </summary> /// <param name="entries">Entries for published_nodes.json</param> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> public static async Task PublishNodesAsync( IIoTPlatformTestContext context, string publishedNodesFullPath, IEnumerable <PublishedNodesEntryModel> entries ) { var json = JsonConvert.SerializeObject(entries, Formatting.Indented); context.OutputHelper?.WriteLine("Write published_nodes.json to IoT Edge"); context.OutputHelper?.WriteLine(json); CreateFolderOnEdgeVM(TestConstants.PublishedNodesFolder, context); using var scpClient = CreateScpClientAndConnect(context); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); scpClient.Upload(stream, publishedNodesFullPath); if (context.IoTEdgeConfig.NestedEdgeFlag == "Enable") { using var sshCient = CreateSshClientAndConnect(context); foreach (var edge in context.IoTEdgeConfig.NestedEdgeSshConnections) { if (edge != string.Empty) { // Copy file to the edge vm var command = $"scp -oStrictHostKeyChecking=no {publishedNodesFullPath} {edge}:{TestConstants.PublishedNodesFilename}"; sshCient.RunCommand(command); // Move file to the target folder with sudo permissions command = $"ssh -oStrictHostKeyChecking=no {edge} 'sudo mv {TestConstants.PublishedNodesFilename} {publishedNodesFullPath}'"; sshCient.RunCommand(command); } } } }
/// <summary> /// Wait until the OPC UA endpoint is detected /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <param name="requestedEndpointUrls">List of OPC UA endpoint URLS that need to be activated and connected</param> /// <returns>content of GET /registry/v2/endpoints request as dynamic object</returns> public static async Task <dynamic> WaitForEndpointDiscoveryToBeCompleted( IIoTPlatformTestContext context, CancellationToken ct = default, IEnumerable <string> requestedEndpointUrls = null) { ct.ThrowIfCancellationRequested(); try { dynamic json; int foundEndpoints = 0; int numberOfItems; bool shouldExit = false; do { json = await GetEndpointInternalAsync(context, ct).ConfigureAwait(false); foundEndpoints = 0; Assert.NotNull(json); numberOfItems = (int)json.items.Count; if (numberOfItems <= 0) { await Task.Delay(TestConstants.DefaultDelayMilliseconds); } else { for (int indexOfOpcUaEndpoint = 0; indexOfOpcUaEndpoint < numberOfItems; indexOfOpcUaEndpoint++) { var endpoint = ((string)json.items[indexOfOpcUaEndpoint].registration.endpoint.url).TrimEnd('/'); if (requestedEndpointUrls == null || requestedEndpointUrls.Contains(endpoint)) { foundEndpoints++; } } var expectedNumberOfEndpoints = requestedEndpointUrls != null ? requestedEndpointUrls.Count() : 1; if (foundEndpoints < expectedNumberOfEndpoints) { await Task.Delay(TestConstants.DefaultDelayMilliseconds); } else { shouldExit = true; } } } while (!shouldExit); return(json); } catch (Exception e) { context.OutputHelper?.WriteLine("Error: OPC UA endpoint not found in time"); PrettyPrintException(e, context.OutputHelper); throw; } }
/// <summary> /// Wait for all API services of IIoT platform to be healthy. /// </summary> /// <param name="context"> Shared Context for E2E testing Industrial IoT Platform </param> /// <param name="ct"> Cancellation token </param> /// <returns></returns> public static async Task WaitForServicesAsync( IIoTPlatformTestContext context, CancellationToken ct = default ) { const string healthyState = "Healthy"; var healthRoutes = new string[] { TestConstants.APIRoutes.RegistryHealth, TestConstants.APIRoutes.PublisherHealth, TestConstants.APIRoutes.TwinHealth, TestConstants.APIRoutes.JobOrchestratorHealth }; try { var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; while (true) { ct.ThrowIfCancellationRequested(); var tasks = new List <Task <IRestResponse> >(); foreach (var healthRoute in healthRoutes) { var request = new RestRequest(Method.GET) { Resource = healthRoute }; tasks.Add(client.ExecuteAsync(request, ct)); } Task.WaitAll(tasks.ToArray()); var healthyServices = tasks .Where(task => task.Result.StatusCode == HttpStatusCode.OK) .Count(task => task.Result.Content == healthyState); if (healthyServices == healthRoutes.Length) { context.OutputHelper?.WriteLine("All API microservices of IIoT platform " + "are running and in healthy state."); return; } await Task.Delay(TestConstants.DefaultDelayMilliseconds, ct); } } catch (Exception) { context.OutputHelper?.WriteLine("Error: not all API microservices of IIoT " + "platform are in healthy state."); throw; } }
/// <summary> /// Gets the application ID associated with the DiscoveryUrl property of <paramref name="context"/> /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="discoveryUrl">Discovery URL the application is registered to</param> /// <param name="ct">Cancellation token</param> public static async Task <string> GetApplicationIdAsync( IIoTPlatformTestContext context, string discoveryUrl, CancellationToken ct = default) { var accessToken = await GetTokenAsync(context, ct).ConfigureAwait(false); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.GET); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = TestConstants.APIRoutes.RegistryApplications; var response = await client.ExecuteAsync(request, ct).ConfigureAwait(false); Assert.NotNull(response); if (!response.IsSuccessful) { context.OutputHelper?.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper?.WriteLine($"ErrorMessage: {response.ErrorMessage}"); Assert.True(response.IsSuccessful, "GET /registry/v2/application failed!"); } dynamic result = JsonConvert.DeserializeObject <ExpandoObject>(response.Content, new ExpandoObjectConverter()); var json = (IDictionary <string, object>)result; Assert.True(HasProperty(result, "items"), "GET /registry/v2/application response did not contain items"); Assert.False(result.items == null, "GET /registry/v2/application response items property is null"); foreach (var item in result.items) { var itemDictionary = (IDictionary <string, object>)item; if (!itemDictionary.ContainsKey("discoveryUrls") || !itemDictionary.ContainsKey("applicationId")) { continue; } var discoveryUrls = (List <object>)item.discoveryUrls; var itemUrl = (string)discoveryUrls?.FirstOrDefault(url => IsUrlStringsEqual(url as string, discoveryUrl)); if (itemUrl != null) { return(item.applicationId); } } return(null); }
/// <summary> /// Gets the private SSH key from the configuration to connect to the Edge VM /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <returns></returns> private static PrivateKeyFile GetPrivateSshKey(IIoTPlatformTestContext context) { context.OutputHelper?.WriteLine("Load private key from environment variable"); var buffer = Encoding.Default.GetBytes(context.SshConfig.PrivateKey); var privateKeyStream = new MemoryStream(buffer); var privateKeyFile = new PrivateKeyFile(privateKeyStream); return(privateKeyFile); }
/// <summary> /// Update Device Twin tag /// </summary> /// <param name="patch">Name of deployed Industrial IoT</param> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> public static async Task UpdateTagAsync( string patch, IIoTPlatformTestContext context, CancellationToken ct = default ) { var registryManager = context.RegistryHelper.RegistryManager; var twin = await registryManager.GetTwinAsync(context.DeviceConfig.DeviceId, ct); await registryManager.UpdateTwinAsync(twin.DeviceId, patch, twin.ETag, ct); }
/// <summary> /// Sets the unmanaged-Tag to "true" to enable Standalone-Mode /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <returns></returns> private static async Task SwitchToStandaloneModeAsync(IIoTPlatformTestContext context, CancellationToken ct = default) { var patch = @"{ tags: { unmanaged: true } }"; await UpdateTagAsync(patch, context, ct); }
/// <summary> /// Activates (and waits for activated and connected state) the endpoint from <paramref name="context"/> /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> public static async Task ActivateEndpointAsync(IIoTPlatformTestContext context, string endpointId, CancellationToken ct = default) { var accessToken = await GetTokenAsync(context, ct).ConfigureAwait(false); Assert.False(string.IsNullOrWhiteSpace(endpointId), "Endpoint not set in the test context"); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.POST); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = string.Format(TestConstants.APIRoutes.RegistryActivateEndpointsFormat, endpointId); // TODO remove workaround // This request used to fail when called for the first time. The bug was fixed in the master, // but we will keep this workaround until we can consume the fixed platform in the E2E tests. bool IsSuccessful = false; int numberOfRetries = 3; while (!IsSuccessful && numberOfRetries > 0) { var response = await client.ExecuteAsync(request, ct); IsSuccessful = response.IsSuccessful; numberOfRetries--; } Assert.True(IsSuccessful, "POST /registry/v2/endpoints/{endpointId}/activate failed!"); while (true) { Assert.False(ct.IsCancellationRequested, "Endpoint was not activated within the expected timeout"); var endpointList = await GetEndpointsAsync(context, ct).ConfigureAwait(false); var endpoint = endpointList.FirstOrDefault(e => string.Equals(e.Id, endpointId)); if (string.Equals(endpoint.ActivationState, TestConstants.StateConstants.ActivatedAndConnected) && string.Equals(endpoint.EndpointState, TestConstants.StateConstants.Ready)) { return; } context.OutputHelper?.WriteLine(string.IsNullOrEmpty(endpoint.Url) ? "Endpoint not found" : $"Endpoint state: {endpoint.EndpointState}, activation: {endpoint.ActivationState}"); await Task.Delay(TestConstants.DefaultDelayMilliseconds).ConfigureAwait(false); } }
/// <summary> /// Read PublishedNodes json from OPC-PLC and provide the data to the tests /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <returns>Dictionary with URL of PLC-PLC as key and Content of Published Nodes files as value</returns> public static async Task <IDictionary <string, PublishedNodesEntryModel> > GetSimulatedPublishedNodesConfigurationAsync( IIoTPlatformTestContext context, CancellationToken ct = default ) { var result = new Dictionary <string, PublishedNodesEntryModel>(); var opcPlcList = context.OpcPlcConfig.Urls; context.OutputHelper?.WriteLine($"SimulatedOpcPlcUrls {opcPlcList}"); var ipAddressList = opcPlcList.Split(TestConstants.SimulationUrlsSeparator); foreach (var ipAddress in ipAddressList.Where(s => !string.IsNullOrWhiteSpace(s))) { try { using (var client = new HttpClient()) { var ub = new UriBuilder { Host = ipAddress }; var baseAddress = ub.Uri; client.BaseAddress = baseAddress; client.Timeout = TimeSpan.FromMilliseconds(TestConstants.DefaultTimeoutInMilliseconds); using (var response = await client.GetAsync(TestConstants.OpcSimulation.PublishedNodesFile, ct)) { Assert.NotNull(response); Assert.True(response.IsSuccessStatusCode, $"http GET request to load pn.json failed, Status {response.StatusCode}"); var json = await response.Content.ReadAsStringAsync(); Assert.NotEmpty(json); var entryModels = JsonConvert.DeserializeObject <PublishedNodesEntryModel[]>(json); Assert.NotNull(entryModels); Assert.NotEmpty(entryModels); Assert.NotNull(entryModels[0].OpcNodes); Assert.NotEmpty(entryModels[0].OpcNodes); // Set endpoint url correctly when it's not specified in pn.json ie. replace fqdn with the ip address string fqdn = Regex.Match(entryModels[0].EndpointUrl, @"opc.tcp:\/\/([^\}]+):").Groups[1].Value; entryModels[0].EndpointUrl = entryModels[0].EndpointUrl.Replace(fqdn, ipAddress); result.Add(ipAddress, entryModels[0]); } } } catch (Exception e) { context.OutputHelper?.WriteLine("Error occurred while downloading Message: {0} skipped: {1}", e.Message, ipAddress); continue; } } return(result); }
/// <summary> /// Request OAuth token using Http basic authentication from environment variables /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <returns>Return content of request token or empty string</returns> public static async Task <string> GetTokenAsync( IIoTPlatformTestContext context, CancellationToken ct = default ) { return(await GetTokenAsync( context.IIoTPlatformConfigHubConfig.AuthTenant, context.IIoTPlatformConfigHubConfig.AuthClientId, context.IIoTPlatformConfigHubConfig.AuthClientSecret, context.IIoTPlatformConfigHubConfig.ApplicationName, ct )); }
/// <summary> /// Sets the unmanaged-Tag to "true" to enable Orchestrated-Mode /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <returns></returns> public static async Task SwitchToOrchestratedModeAsync(IIoTPlatformTestContext context, CancellationToken ct = default) { var patch = @"{ tags: { unmanaged: null } }"; await UpdateTagAsync(patch, context, ct); DeleteFileOnEdgeVM(TestConstants.PublishedNodesFullName, context); }
/// <summary> /// Wait for first OPC UA endpoint to be activated /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <returns>content of GET /registry/v2/endpoints request as dynamic object</returns> public static async Task <dynamic> WaitForEndpointToBeActivatedAsync( IIoTPlatformTestContext context, CancellationToken ct = default) { var accessToken = await TestHelper.GetTokenAsync(context, ct); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; ct.ThrowIfCancellationRequested(); try { dynamic json; string activationState; do { var request = new RestRequest(Method.GET); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = TestConstants.APIRoutes.RegistryEndpoints; var response = await client.ExecuteAsync(request, ct); Assert.NotNull(response); Assert.True(response.IsSuccessful, "GET /registry/v2/endpoints failed!"); if (!response.IsSuccessful) { context.OutputHelper?.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper?.WriteLine($"ErrorMessage: {response.ErrorMessage}"); } Assert.NotEmpty(response.Content); json = JsonConvert.DeserializeObject(response.Content); Assert.NotNull(json); activationState = (string)json.items[0].activationState; // wait the endpoint to be connected if (activationState == "Activated") { await Task.Delay(TestConstants.DefaultTimeoutInMilliseconds, ct); } } while (activationState != "ActivatedAndConnected"); return(json); } catch (Exception) { context.OutputHelper?.WriteLine("Error: OPC UA endpoint couldn't be activated"); throw; } }
/// <summary> /// Delete a file on the Edge VM /// </summary> /// <param name="fileName">Filename of the file to delete</param> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> public static void DeleteFileOnEdgeVM(string fileName, IIoTPlatformTestContext context) { var isSuccessful = false; using var client = CreateSshClientAndConnect(context); var terminal = client.RunCommand("rm " + fileName); if (string.IsNullOrEmpty(terminal.Error) || terminal.Error.ToLowerInvariant().Contains("no such file")) { isSuccessful = true; } Assert.True(isSuccessful, "Delete file was not successful"); }
/// <summary> /// Read PublishedNodes json from OPC-PLC and provide the data to the tests /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <returns>Dictionary with URL of PLC-PLC as key and Content of Published Nodes files as value</returns> public static async Task <IDictionary <string, PublishedNodesEntryModel> > GetSimulatedPublishedNodesConfigurationAsync( IIoTPlatformTestContext context, CancellationToken ct = default ) { var result = new Dictionary <string, PublishedNodesEntryModel>(); var opcPlcUrls = context.OpcPlcConfig.Urls; context.OutputHelper?.WriteLine($"SimulatedOpcPlcUrls {opcPlcUrls}"); var listOfUrls = opcPlcUrls.Split(TestConstants.SimulationUrlsSeparator); foreach (var url in listOfUrls.Where(s => !string.IsNullOrWhiteSpace(s))) { context.OutputHelper?.WriteLine($"Load pn.json from {url}"); try { using (var client = new HttpClient()) { var ub = new UriBuilder { Host = url }; var baseAddress = ub.Uri; client.BaseAddress = baseAddress; client.Timeout = TimeSpan.FromMilliseconds(TestConstants.DefaultTimeoutInMilliseconds); using (var response = await client.GetAsync(TestConstants.OpcSimulation.PublishedNodesFile, ct)) { Assert.NotNull(response); Assert.True(response.IsSuccessStatusCode, $"http GET request to load pn.json failed, Status {response.StatusCode}"); var json = await response.Content.ReadAsStringAsync(); Assert.NotEmpty(json); var entryModels = JsonConvert.DeserializeObject <PublishedNodesEntryModel[]>(json); Assert.NotNull(entryModels); Assert.NotEmpty(entryModels); Assert.NotNull(entryModels[0].OpcNodes); Assert.NotEmpty(entryModels[0].OpcNodes); result.Add(url, entryModels[0]); } } } catch (Exception e) { context.OutputHelper?.WriteLine("Error occurred while downloading Message: {0} skipped: {1}", e.Message, url); continue; } } return(result); }
/// <summary> /// Activates (and waits for activated and connected state) the endpoint from <paramref name="context"/> /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> public static async Task ActivateEndpointAsync(IIoTPlatformTestContext context, string endpointId, CancellationToken ct = default) { var accessToken = await GetTokenAsync(context, ct).ConfigureAwait(false); Assert.False(string.IsNullOrWhiteSpace(endpointId), "Endpoint not set in the test context"); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.POST); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = string.Format(TestConstants.APIRoutes.RegistryActivateEndpointsFormat, endpointId); var response = client.ExecuteAsync(request, ct).GetAwaiter().GetResult(); Assert.NotNull(response); if (!response.IsSuccessful) { context.OutputHelper?.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper?.WriteLine($"ErrorMessage: {response.ErrorMessage}"); Assert.True(response.IsSuccessful, "POST /registry/v2/endpoints/{endpointId}/activate failed!"); } Assert.Empty(response.Content); while (true) { Assert.False(ct.IsCancellationRequested, "Endpoint was not activated within the expected timeout"); var endpointList = await GetEndpointsAsync(context, ct).ConfigureAwait(false); var endpoint = endpointList.FirstOrDefault(e => string.Equals(e.Id, endpointId)); if (string.Equals(endpoint.ActivationState, TestConstants.StateConstants.ActivatedAndConnected) && string.Equals(endpoint.EndpointState, TestConstants.StateConstants.Ready)) { return; } context.OutputHelper?.WriteLine(string.IsNullOrEmpty(endpoint.Url) ? "Endpoint not found" : $"Endpoint state: {endpoint.EndpointState}, activation: {endpoint.ActivationState}"); await Task.Delay(TestConstants.DefaultDelayMilliseconds).ConfigureAwait(false); } }
/// <summary> /// Wait until the OPC UA server is discovered /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> /// <returns>content of GET /registry/v2/application request as dynamic object</returns> public static async Task <dynamic> WaitForDiscoveryToBeCompletedAsync( IIoTPlatformTestContext context, CancellationToken ct = default ) { ct.ThrowIfCancellationRequested(); try { dynamic json; int numberOfItems; do { var accessToken = await TestHelper.GetTokenAsync(context, ct); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.GET); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = TestConstants.APIRoutes.RegistryApplications; var response = await client.ExecuteAsync(request, ct); Assert.NotNull(response); Assert.True(response.IsSuccessful, "GET /registry/v2/application failed!"); if (!response.IsSuccessful) { context.OutputHelper?.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper?.WriteLine($"ErrorMessage: {response.ErrorMessage}"); } Assert.NotEmpty(response.Content); json = JsonConvert.DeserializeObject(response.Content); Assert.NotNull(json); numberOfItems = (int)json.items.Count; } while (numberOfItems <= 0); return(json); } catch (Exception) { context.OutputHelper?.WriteLine("Error: discovery module didn't find OPC UA server in time"); throw; } }
/// <summary> /// Create a folder on Edge VM (if not exists) /// </summary> /// <param name="folderPath">Name of the folder to create.</param> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> private static void CreateFolderOnEdgeVM(string folderPath, IIoTPlatformTestContext context) { Assert.True(!string.IsNullOrWhiteSpace(folderPath), "folder does not exist"); var isSuccessful = false; using var client = CreateSshClientAndConnect(context); var terminal = client.RunCommand("sudo mkdir " + folderPath + ";" + "cd " + folderPath + "; " + "sudo chmod 777 " + folderPath); if (string.IsNullOrEmpty(terminal.Error) || terminal.Error.Contains("File exists")) { isSuccessful = true; } Assert.True(isSuccessful, "Folder creation was not successful"); }
/// <summary> /// Starts monitoring the incoming messages of the IoT Hub and checks for missing values. /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="expectedValuesChangesPerTimestamp">The expected number of value changes per timestamp</param> /// <param name="expectedIntervalOfValueChanges">The expected time difference between values changes in milliseconds</param> /// <param name="expectedMaximalDuration">The time difference between OPC UA Server fires event until Changes Received in IoT Hub in milliseconds </param> /// <param name="ct">Cancellation token</param> /// <returns></returns> public static async Task StartMonitoringIncomingMessagesAsync( IIoTPlatformTestContext context, int expectedValuesChangesPerTimestamp, int expectedIntervalOfValueChanges, int expectedMaximalDuration, CancellationToken ct = default ) { var runtimeUrl = context.TestEventProcessorConfig.TestEventProcessorBaseUrl.TrimEnd('/') + "/Runtime"; var client = new RestClient(runtimeUrl) { Authenticator = new HttpBasicAuthenticator(context.TestEventProcessorConfig.TestEventProcessorUsername, context.TestEventProcessorConfig.TestEventProcessorPassword), }; var body = new { CommandType = CommandEnum.Start, Configuration = new { IoTHubEventHubEndpointConnectionString = context.IoTHubConfig.IoTHubEventHubConnectionString, StorageConnectionString = context.IoTHubConfig.CheckpointStorageConnectionString, ExpectedValueChangesPerTimestamp = expectedValuesChangesPerTimestamp, ExpectedIntervalOfValueChanges = expectedIntervalOfValueChanges, ThresholdValue = expectedIntervalOfValueChanges > 0 ? expectedIntervalOfValueChanges / 10 : 100, ExpectedMaximalDuration = expectedMaximalDuration, } }; var request = new RestRequest("", Method.Put) { Timeout = TestConstants.DefaultTimeoutInMilliseconds, }; request.AddJsonBody(body); var response = await client.ExecuteAsync(request, ct); Assert.True(response.IsSuccessful, $"Response status code: {response.StatusCode}"); dynamic json = JsonConvert.DeserializeObject(response.Content); Assert.NotNull(json); }
/// <summary> /// Create a new ScpClient based on SshConfig and directly connects to host /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <returns>Instance of SshClient, that need to be disposed</returns> private static ScpClient CreateScpClientAndConnect(IIoTPlatformTestContext context) { var privateKeyFile = GetPrivateSshKey(context); context.OutputHelper?.WriteLine("Create SCP Client"); var client = new ScpClient( context.SshConfig.Host, context.SshConfig.Username, privateKeyFile); context.OutputHelper?.WriteLine("open scp connection to host {0} with username {1}", context.SshConfig.Host, context.SshConfig.Username); client.Connect(); context.OutputHelper?.WriteLine("scp connection successful established"); return(client); }
/// <summary> /// Equivalent to recursive calling GetSetOfUniqueNodesAsync to get the whole hierarchy of nodes /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="endpointId">Id of the endpoint as returned by <see cref="Registry_GetEndpoints(IIoTPlatformTestContext)"/></param> /// <param name="nodeClass">Class of the node to filter to or null for no filtering</param> /// <param name="nodeId">Id of the parent node or null to browse the root node</param> /// <param name="ct">Cancellation token</param> public static async Task <List <(string NodeId, string NodeClass, bool Children)> > GetBrowseEndpointRecursiveAsync( IIoTPlatformTestContext context, string endpointId, string nodeClass = null, string nodeId = null, CancellationToken ct = default) { if (string.IsNullOrEmpty(endpointId)) { context.OutputHelper.WriteLine($"{nameof(endpointId)} is null or empty"); throw new ArgumentNullException(nameof(endpointId)); } var nodes = new ConcurrentBag <(string NodeId, string NodeClass, bool Children)>(); await GetBrowseEndpointRecursiveCollectResultsAsync(context, endpointId, nodes, nodeId, ct); return(nodes.Where(n => string.Equals(nodeClass, n.NodeClass, StringComparison.OrdinalIgnoreCase)).ToList()); }
/// <summary> /// transfer the content of published_nodes.json file into the OPC Publisher edge module /// </summary> /// <param name="entries">Entries for published_nodes.json</param> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> public static async Task SwitchToStandaloneModeAndPublishNodesAsync( IEnumerable <PublishedNodesEntryModel> entries, IIoTPlatformTestContext context, CancellationToken ct = default ) { DeleteFileOnEdgeVM(TestConstants.PublishedNodesFullName, context); var json = JsonConvert.SerializeObject(entries, Formatting.Indented); context.OutputHelper?.WriteLine("Write published_nodes.json to IoT Edge"); context.OutputHelper?.WriteLine(json); CreateFolderOnEdgeVM(TestConstants.PublishedNodesFolder, context); using var client = CreateScpClientAndConnect(context); await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); client.Upload(stream, TestConstants.PublishedNodesFullName); await SwitchToStandaloneModeAsync(context, ct); }
/// <summary> /// Waits for the discovery to be completed and then gets the Endpoint ID /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="requestedEndpointUrl">Endpoint URL to get the ID for</param> /// <param name="ct">Cancellation token</param> /// <returns></returns> public static async Task <string> GetOpcUaEndpointId( IIoTPlatformTestContext context, string requestedEndpointUrl, CancellationToken ct) { var json = await WaitForEndpointDiscoveryToBeCompleted(context, ct, new List <string> { requestedEndpointUrl }); int numberOfItems = json.items.Count; for (var indexOfOpcUaEndpoint = 0; indexOfOpcUaEndpoint < numberOfItems; indexOfOpcUaEndpoint++) { var endpoint = ((string)json.items[indexOfOpcUaEndpoint].registration.endpointUrl).TrimEnd('/'); if (endpoint == requestedEndpointUrl) { return((string)json.items[indexOfOpcUaEndpoint].registration.id); } } return(null); }
/// <summary> /// Clean published nodes JSON files for both legacy (2.5) and current (2.8) versions. /// </summary> /// <param name="context"></param> /// <returns></returns> public static async Task CleanPublishedNodesJsonFilesAsync(IIoTPlatformTestContext context) { // Make sure directories exist. using (var sshCient = CreateSshClientAndConnect(context)) { sshCient.RunCommand($"[ ! -d { TestConstants.PublishedNodesFolder} ]" + $" && sudo mkdir -m 777 -p {TestConstants.PublishedNodesFolder}"); sshCient.RunCommand($"[ ! -d { TestConstants.PublishedNodesFolderLegacy} ]" + $" && sudo mkdir -m 777 -p {TestConstants.PublishedNodesFolderLegacy}"); } await PublishNodesAsync( context, TestConstants.PublishedNodesFullName, Array.Empty <PublishedNodesEntryModel>() ).ConfigureAwait(false); await PublishNodesAsync( context, TestConstants.PublishedNodesFullNameLegacy, Array.Empty <PublishedNodesEntryModel>() ).ConfigureAwait(false); }
/// <summary> /// Registers a server, the discovery url will be saved in the <paramref name="context"/> /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="discoveryUrl">Discovery URL to register</param> /// <param name="ct">Cancellation token</param> public static async Task RegisterServerAsync( IIoTPlatformTestContext context, string discoveryUrl, CancellationToken ct = default) { if (string.IsNullOrEmpty(discoveryUrl)) { context.OutputHelper.WriteLine($"{nameof(discoveryUrl)} is null or empty"); throw new ArgumentNullException(nameof(discoveryUrl)); } var accessToken = await GetTokenAsync(context, ct).ConfigureAwait(false); var client = new RestClient(context.IIoTPlatformConfigHubConfig.BaseUrl) { Timeout = TestConstants.DefaultTimeoutInMilliseconds }; var request = new RestRequest(Method.POST); request.AddHeader(TestConstants.HttpHeaderNames.Authorization, accessToken); request.Resource = TestConstants.APIRoutes.RegistryApplications; var body = new { discoveryUrl }; request.AddJsonBody(JsonConvert.SerializeObject(body)); var response = await client.ExecuteAsync(request, ct).ConfigureAwait(false); Assert.NotNull(response); if (!response.IsSuccessful) { context.OutputHelper.WriteLine($"StatusCode: {response.StatusCode}"); context.OutputHelper.WriteLine($"ErrorMessage: {response.ErrorMessage}"); Assert.True(response.IsSuccessful, "POST /registry/v2/application failed!"); } }
/// <summary> /// Gets endpoints from registry /// </summary> /// <param name="context">Shared Context for E2E testing Industrial IoT Platform</param> /// <param name="ct">Cancellation token</param> public static async Task <List <(string Id, string Url, string ActivationState, string EndpointState)> > GetEndpointsAsync( IIoTPlatformTestContext context, CancellationToken ct = default) { dynamic json = await GetEndpointInternalAsync(context, ct).ConfigureAwait(false); Assert.True(HasProperty(json, "items"), "GET /registry/v2/endpoints response has no items"); Assert.False(json.items == null, "GET /registry/v2/endpoints response items property is null"); Assert.NotEqual(0, json.items.Count); var result = new List <(string Id, string Url, string ActivationState, string EndpointState)>(); foreach (var item in json.items) { var id = item.registration.id?.ToString(); var endpointUrl = item.registration.endpointUrl?.ToString(); var activationState = item.activationState?.ToString(); var endpointState = item.endpointState?.ToString(); result.Add((id, endpointUrl, activationState, endpointState)); } return(result); }