Skip to content

Commit 2eeb5c3

Browse files
committed
Add validations for reading cluster config from YAML
1 parent 90b7f7e commit 2eeb5c3

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

KustoSchemaTools.Tests/Parser/YamlClusterHandlerTests.cs

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,232 @@ public async Task LoadAsync_InvalidClustersProperties_ThrowsInvalidOperationExce
116116
File.Delete(tempFilePath);
117117
}
118118
}
119+
120+
[Fact]
121+
public async Task LoadAsync_ClusterMissingName_ThrowsInvalidOperationException()
122+
{
123+
// Arrange
124+
var tempFilePath = Path.GetTempFileName();
125+
try
126+
{
127+
var yamlContent = @"
128+
connections:
129+
- url: test.eastus
130+
workloadGroups: []
131+
";
132+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
133+
var handler = new YamlClusterHandler(tempFilePath);
134+
135+
// Act & Assert
136+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => handler.LoadAsync());
137+
Assert.Contains("Cluster at index 0 is missing a required 'name' property", exception.Message);
138+
}
139+
finally
140+
{
141+
if (File.Exists(tempFilePath))
142+
File.Delete(tempFilePath);
143+
}
144+
}
145+
146+
[Fact]
147+
public async Task LoadAsync_ClusterMissingUrl_ThrowsInvalidOperationException()
148+
{
149+
// Arrange
150+
var tempFilePath = Path.GetTempFileName();
151+
try
152+
{
153+
var yamlContent = @"
154+
connections:
155+
- name: testcluster
156+
workloadGroups: []
157+
";
158+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
159+
var handler = new YamlClusterHandler(tempFilePath);
160+
161+
// Act & Assert
162+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => handler.LoadAsync());
163+
Assert.Contains("Cluster 'testcluster' is missing a required 'url' property", exception.Message);
164+
}
165+
finally
166+
{
167+
if (File.Exists(tempFilePath))
168+
File.Delete(tempFilePath);
169+
}
170+
}
171+
172+
[Fact]
173+
public async Task LoadAsync_WorkloadGroupMissingName_ThrowsInvalidOperationException()
174+
{
175+
// Arrange
176+
var tempFilePath = Path.GetTempFileName();
177+
try
178+
{
179+
var yamlContent = @"
180+
connections:
181+
- name: testcluster
182+
url: testcluster.eastus
183+
workloadGroups:
184+
- workloadGroupPolicy:
185+
requestRateLimitsEnforcementPolicy:
186+
commandsEnforcementLevel: Cluster
187+
- workloadGroupName: validgroup
188+
workloadGroupPolicy:
189+
requestRateLimitPolicies: []
190+
";
191+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
192+
var handler = new YamlClusterHandler(tempFilePath);
193+
194+
// Act & Assert
195+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => handler.LoadAsync());
196+
Assert.Contains("Cluster 'testcluster' has a workload group at index 0 that is missing a required 'workloadGroupName' property", exception.Message);
197+
Assert.Contains("All workload groups must have a non-empty name", exception.Message);
198+
}
199+
finally
200+
{
201+
if (File.Exists(tempFilePath))
202+
File.Delete(tempFilePath);
203+
}
204+
}
205+
206+
[Fact]
207+
public async Task LoadAsync_WorkloadGroupEmptyName_ThrowsInvalidOperationException()
208+
{
209+
// Arrange
210+
var tempFilePath = Path.GetTempFileName();
211+
try
212+
{
213+
var yamlContent = @"
214+
connections:
215+
- name: testcluster
216+
url: testcluster.eastus
217+
workloadGroups:
218+
- workloadGroupName: ''
219+
workloadGroupPolicy:
220+
requestRateLimitPolicies: []
221+
";
222+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
223+
var handler = new YamlClusterHandler(tempFilePath);
224+
225+
// Act & Assert
226+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => handler.LoadAsync());
227+
Assert.Contains("Cluster 'testcluster' has a workload group at index 0 that is missing a required 'workloadGroupName' property", exception.Message);
228+
}
229+
finally
230+
{
231+
if (File.Exists(tempFilePath))
232+
File.Delete(tempFilePath);
233+
}
234+
}
235+
236+
[Fact]
237+
public async Task LoadAsync_WorkloadGroupWhitespaceOnlyName_ThrowsInvalidOperationException()
238+
{
239+
// Arrange
240+
var tempFilePath = Path.GetTempFileName();
241+
try
242+
{
243+
var yamlContent = @"
244+
connections:
245+
- name: testcluster
246+
url: testcluster.eastus
247+
workloadGroups:
248+
- workloadGroupName: ' '
249+
workloadGroupPolicy:
250+
requestRateLimitPolicies: []
251+
";
252+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
253+
var handler = new YamlClusterHandler(tempFilePath);
254+
255+
// Act & Assert
256+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => handler.LoadAsync());
257+
Assert.Contains("Cluster 'testcluster' has a workload group at index 0 that is missing a required 'workloadGroupName' property", exception.Message);
258+
}
259+
finally
260+
{
261+
if (File.Exists(tempFilePath))
262+
File.Delete(tempFilePath);
263+
}
264+
}
265+
266+
[Fact]
267+
public async Task LoadAsync_ValidWorkloadGroups_DoesNotThrow()
268+
{
269+
// Arrange
270+
var tempFilePath = Path.GetTempFileName();
271+
try
272+
{
273+
var yamlContent = @"
274+
connections:
275+
- name: testcluster
276+
url: testcluster.eastus
277+
workloadGroups:
278+
- workloadGroupName: group1
279+
workloadGroupPolicy:
280+
requestRateLimitPolicies:
281+
- limitKind: ConcurrentRequests
282+
scope: WorkloadGroup
283+
isEnabled: true
284+
properties:
285+
maxConcurrentRequests: 100
286+
- workloadGroupName: group2
287+
workloadGroupPolicy:
288+
requestRateLimitsEnforcementPolicy:
289+
commandsEnforcementLevel: Cluster
290+
";
291+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
292+
var handler = new YamlClusterHandler(tempFilePath);
293+
294+
// Act
295+
var result = await handler.LoadAsync();
296+
297+
// Assert
298+
Assert.NotNull(result);
299+
Assert.Single(result);
300+
var cluster = result[0];
301+
Assert.Equal("testcluster", cluster.Name);
302+
Assert.Equal("testcluster.eastus", cluster.Url);
303+
Assert.Equal(2, cluster.WorkloadGroups.Count);
304+
Assert.Equal("group1", cluster.WorkloadGroups[0].WorkloadGroupName);
305+
Assert.Equal("group2", cluster.WorkloadGroups[1].WorkloadGroupName);
306+
}
307+
finally
308+
{
309+
if (File.Exists(tempFilePath))
310+
File.Delete(tempFilePath);
311+
}
312+
}
313+
314+
[Fact]
315+
public async Task LoadAsync_ClusterWithoutWorkloadGroups_DoesNotThrow()
316+
{
317+
// Arrange
318+
var tempFilePath = Path.GetTempFileName();
319+
try
320+
{
321+
var yamlContent = @"
322+
connections:
323+
- name: testcluster
324+
url: testcluster.eastus
325+
";
326+
await File.WriteAllTextAsync(tempFilePath, yamlContent);
327+
var handler = new YamlClusterHandler(tempFilePath);
328+
329+
// Act
330+
var result = await handler.LoadAsync();
331+
332+
// Assert
333+
Assert.NotNull(result);
334+
Assert.Single(result);
335+
var cluster = result[0];
336+
Assert.Equal("testcluster", cluster.Name);
337+
Assert.Equal("testcluster.eastus", cluster.Url);
338+
Assert.Empty(cluster.WorkloadGroups);
339+
}
340+
finally
341+
{
342+
if (File.Exists(tempFilePath))
343+
File.Delete(tempFilePath);
344+
}
345+
}
119346
}
120347
}

KustoSchemaTools/Parser/YamlClusterHandler.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,52 @@ public async Task<List<Cluster>> LoadAsync()
3030

3131
var clusters = Serialization.YamlPascalCaseDeserializer.Deserialize<Clusters>(clustersFileContent);
3232

33+
ValidateClusters(clusters);
34+
3335
return clusters.Connections.ToList();
3436
}
3537
catch (Exception ex) when (!(ex is FileNotFoundException || ex is InvalidOperationException))
3638
{
3739
throw new InvalidOperationException($"Failed to parse clusters file '{_filePath}': {ex.Message}", ex);
3840
}
3941
}
42+
43+
private static void ValidateClusters(Clusters clusters)
44+
{
45+
if (clusters?.Connections == null)
46+
return;
47+
48+
for (int clusterIndex = 0; clusterIndex < clusters.Connections.Count; clusterIndex++)
49+
{
50+
var cluster = clusters.Connections[clusterIndex];
51+
52+
// Validate cluster basic properties
53+
if (string.IsNullOrWhiteSpace(cluster.Name))
54+
{
55+
throw new InvalidOperationException($"Cluster at index {clusterIndex} is missing a required 'name' property.");
56+
}
57+
58+
if (string.IsNullOrWhiteSpace(cluster.Url))
59+
{
60+
throw new InvalidOperationException($"Cluster '{cluster.Name}' is missing a required 'url' property.");
61+
}
62+
63+
// Validate workload groups
64+
if (cluster.WorkloadGroups?.Count > 0)
65+
{
66+
for (int wgIndex = 0; wgIndex < cluster.WorkloadGroups.Count; wgIndex++)
67+
{
68+
var workloadGroup = cluster.WorkloadGroups[wgIndex];
69+
70+
if (string.IsNullOrWhiteSpace(workloadGroup.WorkloadGroupName))
71+
{
72+
throw new InvalidOperationException(
73+
$"Cluster '{cluster.Name}' has a workload group at index {wgIndex} that is missing a required 'workloadGroupName' property. " +
74+
"All workload groups must have a non-empty name.");
75+
}
76+
}
77+
}
78+
}
79+
}
4080
}
4181
}

0 commit comments

Comments
 (0)