Skip to content

Commit 2d7b193

Browse files
committed
feat(api): Query Parameter support for Load Methods and Serialize handling
1 parent a1ace8e commit 2d7b193

4 files changed

Lines changed: 295 additions & 33 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Reflection;
5+
using System.Threading.Tasks;
6+
using Kepware.Api.Model;
7+
using Kepware.Api.Test.ApiClient;
8+
using Microsoft.Extensions.Logging;
9+
using Moq;
10+
using Moq.Contrib.HttpClient;
11+
using Xunit;
12+
13+
namespace Kepware.Api.Test.ApiClient
14+
{
15+
public class GenericHandler : TestApiClientBase
16+
{
17+
[Fact]
18+
public void AppendQueryString_PrivateMethod_EncodesAndSkipsNullsAndAppendsCorrectly()
19+
{
20+
// Arrange
21+
var method = typeof(Kepware.Api.ClientHandler.GenericApiHandler)
22+
.GetMethod("AppendQueryString", BindingFlags.NonPublic | BindingFlags.Static);
23+
Assert.NotNull(method);
24+
25+
var query = new[]
26+
{
27+
new KeyValuePair<string, string?>("a", "b"),
28+
new KeyValuePair<string, string?>("space", "x y"),
29+
new KeyValuePair<string, string?>("skip", null) // should be skipped
30+
};
31+
32+
// Act
33+
var result1 = (string)method!.Invoke(null, new object[] { "https://api/config", query })!;
34+
var result2 = (string)method!.Invoke(null, new object[] { "https://api/config?existing=1", query })!;
35+
36+
// Assert
37+
Assert.Equal("https://api/config?a=b&space=x%20y", result1);
38+
Assert.Equal("https://api/config?existing=1&a=b&space=x%20y", result2);
39+
}
40+
41+
[Fact]
42+
public async Task LoadCollectionAsync_AppendsQueryAndReturnsCollection()
43+
{
44+
// Arrange
45+
var channelsJson = """
46+
[
47+
{
48+
"PROJECT_ID": 676550906,
49+
"common.ALLTYPES_NAME": "Channel1",
50+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
51+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator"
52+
},
53+
{
54+
"PROJECT_ID": 676550906,
55+
"common.ALLTYPES_NAME": "Data Type Examples",
56+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
57+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator"
58+
}
59+
]
60+
""";
61+
62+
var query = new[]
63+
{
64+
new KeyValuePair<string, string?>("status", "active"),
65+
new KeyValuePair<string, string?>("name", "John Doe"),
66+
new KeyValuePair<string, string?>("skip", null)
67+
};
68+
69+
// Expect encoded space in "John Doe" and null entry skipped
70+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels?status=active&name=John%20Doe")
71+
.ReturnsResponse(channelsJson, "application/json");
72+
73+
// Act
74+
var result = await _kepwareApiClient.GenericConfig.LoadCollectionAsync<ChannelCollection, Channel>((string?)null, query);
75+
76+
// Assert
77+
Assert.NotNull(result);
78+
Assert.Equal(2, result.Count);
79+
Assert.Contains(result, c => c.Name == "Channel1");
80+
Assert.Contains(result, c => c.Name == "Data Type Examples");
81+
}
82+
83+
[Fact]
84+
public async Task LoadEntityAsync_AppendsQueryAndReturnsEntity()
85+
{
86+
// Arrange
87+
var channelJson = """
88+
{
89+
"PROJECT_ID": 676550906,
90+
"common.ALLTYPES_NAME": "Channel1",
91+
"common.ALLTYPES_DESCRIPTION": "Example Simulator Channel",
92+
"servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator"
93+
}
94+
""";
95+
96+
var query = new[]
97+
{
98+
new KeyValuePair<string, string?>("status", "active"),
99+
new KeyValuePair<string, string?>("name", "John Doe"),
100+
new KeyValuePair<string, string?>("skip", null)
101+
};
102+
103+
// Expect encoded space in "John Doe" and null entry skipped
104+
_httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?status=active&name=John%20Doe")
105+
.ReturnsResponse(channelJson, "application/json");
106+
107+
// Act
108+
var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync<Channel>("Channel1", query);
109+
110+
// Assert
111+
Assert.NotNull(result);
112+
Assert.Equal("Channel1", result.Name);
113+
Assert.Equal("Example Simulator Channel", result.Description);
114+
}
115+
}
116+
}

Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ public async Task LoadEntityAsync_ShouldReturnTagCollection_WhenApiRespondsSucce
276276
[Fact]
277277
public async Task LoadEntityAsync_ShouldReturnTagGroupCollectionInTagGroup_WhenApiRespondsSuccessfully()
278278
{
279-
// TODO: Currently this test fails due to issue in EndpointResolver.
279+
// TODO: Clean up test. Fix was made and test is successful.
280280
// Arrange
281281
try
282282
{
@@ -335,5 +335,46 @@ public async Task LoadEntityAsync_ShouldReturnTagCollectionFromTagGroup_WhenApiR
335335
await DeleteAllChannelsAsync();
336336
}
337337
#endregion
338+
339+
#region LoadEntityAsync - Return Channel Collection with all children - Serialize query
340+
[SkippableFact]
341+
public async Task LoadEntityAsync_ShouldReturnChannelAndChildren_WhenApiRespondsSuccessfully()
342+
{
343+
// Skip the test if the serialize feature is not supported by server version.
344+
Skip.If(!_productInfo.SupportsJsonProjectLoadService, "Test only applicable for versions that support JsonProjectLoad");
345+
346+
// Arrange
347+
var channel = await AddTestChannel();
348+
var device = await AddTestDevice(channel);
349+
var tagGroup = await AddTestTagGroup(device);
350+
var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2");
351+
352+
var query = new[]
353+
{
354+
new KeyValuePair<string, string?>("content", "serialize"),
355+
};
356+
357+
// Act
358+
var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync<Channel>(channel.Name, query);
359+
360+
// Assert
361+
Assert.NotNull(result);
362+
Assert.NotNull(result.Devices);
363+
Assert.Contains(result.Devices, g => g.Name == device.Name);
364+
365+
var foundDevice = result.Devices.Find(d => d.Name == device.Name);
366+
Assert.NotNull(foundDevice);
367+
Assert.NotNull(foundDevice.TagGroups);
368+
Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name);
369+
370+
var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name);
371+
Assert.NotNull(foundTagGroup);
372+
Assert.NotNull(foundTagGroup.TagGroups);
373+
Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name);
374+
375+
// Cleanup
376+
await DeleteAllChannelsAsync();
377+
}
378+
#endregion
338379
}
339380
}

Kepware.Api/ClientHandler/AdminApiHandler.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public AdminApiHandler(KepwareApiClient kepwareApiClient, ILogger<AdminApiHandle
4242
/// <returns>The current <see cref="AdminSettings"/> or null if retrieval fails.</returns>
4343
public Task<AdminSettings?> GetAdminSettingsAsync(CancellationToken cancellationToken = default)
4444
{
45-
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<AdminSettings>(name: null, cancellationToken);
45+
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<AdminSettings>(name: null, cancellationToken: cancellationToken);
4646
}
4747

4848
/// <summary>
@@ -117,7 +117,7 @@ public async Task<bool> SetAdminSettingsAsync(AdminSettings settings, Cancellati
117117
/// <returns>The <see cref="UaEndpoint"/> configuration, or null if not found.</returns>
118118
public Task<UaEndpoint?> GetUaEndpointAsync(string name, CancellationToken cancellationToken = default)
119119
{
120-
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<UaEndpoint>(name, cancellationToken);
120+
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<UaEndpoint>(name, cancellationToken: cancellationToken);
121121
}
122122

123123
/// <summary>
@@ -135,7 +135,7 @@ public async Task<bool> CreateOrUpdateUaEndpointAsync(UaEndpoint endpoint, Cance
135135
try
136136
{
137137
var endpointUrl = EndpointResolver.ResolveEndpoint<UaEndpoint>([endpoint.Name]);
138-
var currentEndpoint = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<UaEndpoint>(endpointUrl, cancellationToken);
138+
var currentEndpoint = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<UaEndpoint>(endpointUrl, cancellationToken: cancellationToken);
139139

140140
if (currentEndpoint == null)
141141
{
@@ -162,7 +162,7 @@ public async Task<bool> CreateOrUpdateUaEndpointAsync(UaEndpoint endpoint, Cance
162162
/// <param name="cancellationToken">A token that can be used to request cancellation of the operation.</param>
163163
/// <returns>True if the endpoint was successfully deleted; otherwise, false.</returns>
164164
public Task<bool> DeleteUaEndpointAsync(string name, CancellationToken cancellationToken = default)
165-
=> m_kepwareApiClient.GenericConfig.DeleteItemAsync<UaEndpoint>(name, cancellationToken);
165+
=> m_kepwareApiClient.GenericConfig.DeleteItemAsync<UaEndpoint>(name, cancellationToken: cancellationToken);
166166

167167
#endregion
168168

@@ -186,7 +186,7 @@ public Task<bool> DeleteUaEndpointAsync(string name, CancellationToken cancellat
186186
/// <returns>The <see cref="ServerUserGroup"/> configuration, or null if not found.</returns>
187187
public Task<ServerUserGroup?> GetServerUserGroupAsync(string name, CancellationToken cancellationToken = default)
188188
{
189-
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<ServerUserGroup>(name, cancellationToken);
189+
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<ServerUserGroup>(name, cancellationToken: cancellationToken);
190190
}
191191

192192
/// <summary>
@@ -204,7 +204,7 @@ public async Task<bool> CreateOrUpdateServerUserGroupAsync(ServerUserGroup userG
204204
try
205205
{
206206
var endpointUrl = EndpointResolver.ResolveEndpoint<ServerUserGroup>([userGroup.Name]);
207-
var currentGroup = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ServerUserGroup>(endpointUrl, cancellationToken);
207+
var currentGroup = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ServerUserGroup>(endpointUrl, cancellationToken: cancellationToken);
208208

209209
if (currentGroup == null)
210210
{
@@ -231,7 +231,7 @@ public async Task<bool> CreateOrUpdateServerUserGroupAsync(ServerUserGroup userG
231231
/// <param name="cancellationToken">A token that can be used to request cancellation of the operation.</param>
232232
/// <returns>True if the group was successfully deleted; otherwise, false.</returns>
233233
public Task<bool> DeleteServerUserGroupAsync(string name, CancellationToken cancellationToken = default)
234-
=> m_kepwareApiClient.GenericConfig.DeleteItemAsync<ServerUserGroup>(name, cancellationToken);
234+
=> m_kepwareApiClient.GenericConfig.DeleteItemAsync<ServerUserGroup>(name, cancellationToken: cancellationToken);
235235

236236
#endregion
237237

@@ -254,7 +254,7 @@ public Task<bool> DeleteServerUserGroupAsync(string name, CancellationToken canc
254254
/// <returns>The <see cref="ServerUser"/> configuration, or null if not found.</returns>
255255
public Task<ServerUser?> GetServerUserAsync(string name, CancellationToken cancellationToken = default)
256256
{
257-
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<ServerUser>(name, cancellationToken);
257+
return m_kepwareApiClient.GenericConfig.LoadEntityAsync<ServerUser>(name, cancellationToken: cancellationToken);
258258
}
259259

260260
/// <summary>
@@ -272,7 +272,7 @@ public async Task<bool> CreateOrUpdateServerUserAsync(ServerUser user, Cancellat
272272
try
273273
{
274274
var endpointUrl = EndpointResolver.ResolveEndpoint<ServerUser>([user.Name]);
275-
var currentUser = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ServerUser>(endpointUrl, cancellationToken);
275+
var currentUser = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ServerUser>(endpointUrl, cancellationToken: cancellationToken);
276276

277277
if (currentUser == null)
278278
{
@@ -309,7 +309,7 @@ public async Task<bool> CreateOrUpdateServerUserAsync(ServerUser user, Cancellat
309309
/// <param name="cancellationToken">A token that can be used to request cancellation of the operation.</param>
310310
/// <returns>True if the user was successfully deleted; otherwise, false.</returns>
311311
public Task<bool> DeleteServerUserAsync(string name, CancellationToken cancellationToken = default)
312-
=> m_kepwareApiClient.GenericConfig.DeleteItemAsync<ServerUser>(name, cancellationToken);
312+
=> m_kepwareApiClient.GenericConfig.DeleteItemAsync<ServerUser>(name, cancellationToken: cancellationToken);
313313

314314
#endregion
315315

@@ -345,7 +345,7 @@ public Task<bool> DeleteServerUserAsync(string name, CancellationToken cancellat
345345
public Task<ProjectPermission?> GetProjectPermissionAsync(string serverUserGroupName, ProjectPermissionName projectPermissionName, CancellationToken cancellationToken = default)
346346
{
347347
var endpoint = EndpointResolver.ResolveEndpoint<ProjectPermission>([serverUserGroupName, projectPermissionName]);
348-
return m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ProjectPermission>(endpoint, cancellationToken);
348+
return m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ProjectPermission>(endpoint, cancellationToken: cancellationToken);
349349
}
350350

351351
/// <summary>
@@ -371,7 +371,7 @@ public async Task<bool> UpdateProjectPermissionAsync(string serverUserGroupName,
371371
try
372372
{
373373
var endpointUrl = EndpointResolver.ResolveEndpoint<ProjectPermission>([serverUserGroupName, projectPermission.Name]);
374-
var existingPermission = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ProjectPermission>(endpointUrl, cancellationToken);
374+
var existingPermission = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync<ProjectPermission>(endpointUrl, cancellationToken: cancellationToken);
375375

376376
if (existingPermission == null)
377377
{

0 commit comments

Comments
 (0)