Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Commit

Permalink
Make tree path validation more testable
Browse files Browse the repository at this point in the history
  • Loading branch information
kananb committed Sep 11, 2023
1 parent 757a63a commit 49bb44c
Showing 1 changed file with 51 additions and 29 deletions.
80 changes: 51 additions & 29 deletions src/ApiService/ApiService/onefuzzlib/notifications/Ado.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,71 +89,89 @@ private static bool IsTransient(Exception e) {
return errorCodes.Any(errorStr.Contains);
}

private static OneFuzzResultVoid ValidatePathNaming(string path, string? pathType) {
// Validate path based on https://learn.microsoft.com/en-us/azure/devops/organizations/settings/about-areas-iterations?view=azure-devops#naming-restrictions
public static OneFuzzResultVoid ValidateTreePath(IEnumerable<string> path, WorkItemClassificationNode root) {
string treeNodeTypeName;
switch (root.StructureType) {
case TreeNodeStructureType.Area:
treeNodeTypeName = "Area";
break;
case TreeNodeStructureType.Iteration:
treeNodeTypeName = "Iteration";
break;
default:
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"Path root \"{root.Name}\" is an unsupported type. Expected Area or Iteration but got {root.StructureType}.",
});
}

var pathParts = path.Split('\\');
var maxLength = 255;
// Validate path based on
// https://learn.microsoft.com/en-us/azure/devops/organizations/settings/about-areas-iterations?view=azure-devops#naming-restrictions
var maxNodeLength = 255;
var maxDepth = 13;
// Invalid characters from the link above plus the escape sequences (since they have backslashes and produce confusingly formatted errors if not caught here)
var invalidChars = new char[] { '/', '.', '~', '[', ']', ':', '$', '?', '*', '<', '>', '|', '+', '=', ';', ',' };
var escapeChars = new char[] { '\0', '\a', '\b', '\f', '\n', '\r', '\t', '\v' };

// Ensure that none of the path parts are too long
var erroneous = pathParts.FirstOrDefault(part => part.Length > maxLength);
var erroneous = path.FirstOrDefault(part => part.Length > maxNodeLength);
if (erroneous != null) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"{pathType} Path \"{path}\" is invalid. \"{erroneous}\" is too long. It must be less than {maxLength} characters.",
$"{treeNodeTypeName} Path \"{path}\" is invalid. \"{erroneous}\" is too long. It must be less than {maxNodeLength} characters.",
"Learn more about naming restrictions here: https://learn.microsoft.com/en-us/azure/devops/organizations/settings/about-areas-iterations?view=azure-devops#naming-restrictions"
});
}

// Ensure that none of the path parts contain invalid characters
erroneous = pathParts.FirstOrDefault(part => invalidChars.Any(part.Contains));
erroneous = path.FirstOrDefault(part => invalidChars.Any(part.Contains) || escapeChars.Any(part.Contains));
if (erroneous != null) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"{pathType} Path \"{path}\" is invalid. \"{erroneous}\" contains an invalid character ({string.Join(" ", invalidChars)}).",
$"{treeNodeTypeName} Path \"{path}\" is invalid. \"{erroneous}\" contains an invalid character ({string.Join(" ", invalidChars)} \\0 \\a \\b \\f \\n \\r \\t \\v).",
"Make sure that the path is separated by backslashes (\\) and not forward slashes (/).",
"Learn more about naming restrictions here: https://learn.microsoft.com/en-us/azure/devops/organizations/settings/about-areas-iterations?view=azure-devops#naming-restrictions"
});
}

// Ensure that there aren't too many path parts
if (pathParts.Length > maxDepth) {
// Ensure no unicode escape characters
erroneous = path.FirstOrDefault(part => part.Any(ch => char.IsControl(ch)));
if (erroneous != null) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"{pathType} Path \"{path}\" is invalid. It must be less than {maxDepth} levels deep.",
$"{treeNodeTypeName} Path \"{path}\" is invalid. \"{erroneous}\" contains a unicode escape character.",
"Learn more about naming restrictions here: https://learn.microsoft.com/en-us/azure/devops/organizations/settings/about-areas-iterations?view=azure-devops#naming-restrictions"
});
}

return OneFuzzResultVoid.Ok;
}
private static async Async.Task<OneFuzzResultVoid> ValidatePath(string project, string path, TreeStructureGroup structureGroup, WorkItemTrackingHttpClient client) {
var pathType = (structureGroup == TreeStructureGroup.Areas) ? "Area" : "Iteration";
var pathParts = path.Split('\\');

var namingValidation = ValidatePathNaming(path, pathType);
if (!namingValidation.IsOk) {
return namingValidation;
// Ensure that there aren't too many path parts
if (path.Count() > maxDepth) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"{treeNodeTypeName} Path \"{path}\" is invalid. It must be less than {maxDepth} levels deep.",
"Learn more about naming restrictions here: https://learn.microsoft.com/en-us/azure/devops/organizations/settings/about-areas-iterations?view=azure-devops#naming-restrictions"
});
}


// Path should always start with the project name ADO expects an absolute path
if (!string.Equals(pathParts[0], project, StringComparison.OrdinalIgnoreCase)) {
if (!string.Equals(path.First(), root.Name, StringComparison.OrdinalIgnoreCase)) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"Path \"{path}\" is invalid. It must start with the project name, \"{project}\".",
$"Example: \"{project}\\{path}\".",
$"{treeNodeTypeName} Path \"{path}\" is invalid. It must start with the project name, \"{root.Name}\".",
$"Example: \"{root.Name}\\{path}\".",
});
}

// Validate that the project exists
var current = await client.GetClassificationNodeAsync(project, structureGroup, depth: pathParts.Length - 1);
var current = root;
if (current == null) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"{pathType} Path \"{path}\" is invalid. \"{project}\" is not a valid project.",
$"{treeNodeTypeName} Path \"{path}\" is invalid. \"{root.Name}\" is not a valid project.",
"Make sure that your path starts with a valid project name.",
});
}

// Validate that each part of the path is a valid child of the previous part
foreach (var part in pathParts.Skip(1)) {
foreach (var part in path.Skip(1)) {
var child = current.Children?.FirstOrDefault(x => string.Equals(x.Name, part, StringComparison.OrdinalIgnoreCase));
if (child == null) {
return OneFuzzResultVoid.Error(ErrorCode.ADO_VALIDATION_INVALID_PATH, new string[] {
$"{pathType} Path \"{path}\" is invalid. \"{part}\" is not a valid child of \"{current.Name}\".",
$"{treeNodeTypeName} Path \"{path}\" is invalid. \"{part}\" is not a valid child of \"{current.Name}\".",
$"Valid children of \"{current.Name}\" are: [{string.Join(',', current.Children?.Select(x => $"\"{x.Name}\"") ?? new List<string>())}].",
});
}
Expand Down Expand Up @@ -239,13 +257,17 @@ await policy.ExecuteAsync(async () => {
try {
// Validate AreaPath and IterationPath exist
if (config.AdoFields.TryGetValue("System.AreaPath", out var areaPathString)) {
var validateAreaPath = await ValidatePath(config.Project, areaPathString, TreeStructureGroup.Areas, witClient);
var path = areaPathString.Split('\\');
var root = await witClient.GetClassificationNodeAsync(config.Project, TreeStructureGroup.Areas, depth: path.Length - 1);
var validateAreaPath = ValidateTreePath(path, root);
if (!validateAreaPath.IsOk) {
return validateAreaPath;
}
}
if (config.AdoFields.TryGetValue("System.IterationPath", out var iterationPathString)) {
var validateIterationPath = await ValidatePath(config.Project, iterationPathString, TreeStructureGroup.Iterations, witClient);
var path = iterationPathString.Split('\\');
var root = await witClient.GetClassificationNodeAsync(config.Project, TreeStructureGroup.Iterations, depth: path.Length - 1);
var validateIterationPath = ValidateTreePath(path, root);
if (!validateIterationPath.IsOk) {
return validateIterationPath;
}
Expand Down

0 comments on commit 49bb44c

Please sign in to comment.