Skip to content

Commit 60a1c83

Browse files
authored
Bug/fix form validation logic (#68)
* Fixed form validation orchestrator
1 parent 36ffbeb commit 60a1c83

3 files changed

Lines changed: 343 additions & 14 deletions

File tree

src/DfE.ExternalApplications.Application/Interfaces/IFormValidationOrchestrator.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,16 @@ public interface IFormValidationOrchestrator
4343
/// <param name="fieldKey">The field key for model state</param>
4444
/// <returns>True if validation passes</returns>
4545
bool ValidateField(Domain.Models.Field field, object value, ModelStateDictionary modelState, string fieldKey);
46+
47+
/// <summary>
48+
/// Validates a single field with full form data context for conditional validation
49+
/// </summary>
50+
/// <param name="field">The field to validate</param>
51+
/// <param name="value">The field value</param>
52+
/// <param name="formData">The complete form data for conditional evaluation</param>
53+
/// <param name="modelState">The model state to add errors to</param>
54+
/// <param name="fieldKey">The field key for model state</param>
55+
/// <returns>True if validation passes</returns>
56+
bool ValidateField(Domain.Models.Field field, object value, Dictionary<string, object>? formData, ModelStateDictionary modelState, string fieldKey);
4657
}
4758
}

src/DfE.ExternalApplications.Infrastructure/Services/FormValidationOrchestrator.cs

Lines changed: 225 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.AspNetCore.Mvc.ModelBinding;
44
using System.Text.RegularExpressions;
55
using Microsoft.Extensions.Logging;
6+
using System.Text.Json;
67
using Task = DfE.ExternalApplications.Domain.Models.Task;
78

89
namespace DfE.ExternalApplications.Infrastructure.Services
@@ -13,10 +14,12 @@ namespace DfE.ExternalApplications.Infrastructure.Services
1314
public class FormValidationOrchestrator : IFormValidationOrchestrator
1415
{
1516
private readonly ILogger<FormValidationOrchestrator> _logger;
17+
private readonly IConditionalLogicEngine _conditionalLogicEngine;
1618

17-
public FormValidationOrchestrator(ILogger<FormValidationOrchestrator> logger)
19+
public FormValidationOrchestrator(ILogger<FormValidationOrchestrator> logger, IConditionalLogicEngine conditionalLogicEngine)
1820
{
1921
_logger = logger;
22+
_conditionalLogicEngine = conditionalLogicEngine;
2023
}
2124

2225
/// <summary>
@@ -40,7 +43,7 @@ public bool ValidatePage(Page page, Dictionary<string, object> data, ModelStateD
4043
data.TryGetValue(key, out var rawValue);
4144
var value = rawValue?.ToString() ?? string.Empty;
4245

43-
if (!ValidateField(field, value, modelState, key))
46+
if (!ValidateField(field, value, data, modelState, key))
4447
{
4548
isValid = false;
4649
}
@@ -113,6 +116,21 @@ public bool ValidateApplication(FormTemplate template, Dictionary<string, object
113116
/// <param name="fieldKey">The field key for model state</param>
114117
/// <returns>True if validation passes</returns>
115118
public bool ValidateField(Field field, object value, ModelStateDictionary modelState, string fieldKey)
119+
{
120+
// Call the overloaded method with null data for backward compatibility
121+
return ValidateField(field, value, null, modelState, fieldKey);
122+
}
123+
124+
/// <summary>
125+
/// Validates a single field with full form data context for conditional validation
126+
/// </summary>
127+
/// <param name="field">The field to validate</param>
128+
/// <param name="value">The field value</param>
129+
/// <param name="formData">The complete form data for conditional evaluation</param>
130+
/// <param name="modelState">The model state to add errors to</param>
131+
/// <param name="fieldKey">The field key for model state</param>
132+
/// <returns>True if validation passes</returns>
133+
public bool ValidateField(Field field, object value, Dictionary<string, object>? formData, ModelStateDictionary modelState, string fieldKey)
116134
{
117135
if (field?.Validations == null)
118136
{
@@ -122,14 +140,39 @@ public bool ValidateField(Field field, object value, ModelStateDictionary modelS
122140
var stringValue = value?.ToString() ?? string.Empty;
123141
var isValid = true;
124142

143+
// Special handling for complex fields (upload, autocomplete, etc.)
144+
if (field.Type == "complexField" && field.ComplexField != null)
145+
{
146+
return ValidateComplexField(field, value, formData, modelState, fieldKey);
147+
}
148+
125149
foreach (var rule in field.Validations)
126150
{
127-
// Conditional application
151+
// Check if this is a conditional validation rule
128152
if (rule.Condition != null)
129153
{
130-
// This would need access to the full data dictionary to check conditions
131-
// For now, we'll skip conditional validation in this context
132-
continue;
154+
if (formData == null)
155+
{
156+
_logger.LogWarning("Conditional validation rule found for field '{FieldId}' but no form data provided for evaluation. Skipping rule.", field.FieldId);
157+
continue;
158+
}
159+
160+
try
161+
{
162+
// Evaluate the condition using the conditional logic engine
163+
bool conditionMet = _conditionalLogicEngine.EvaluateCondition(rule.Condition, formData);
164+
165+
if (!conditionMet)
166+
{
167+
// Condition not met, skip this validation rule
168+
continue;
169+
}
170+
}
171+
catch (Exception ex)
172+
{
173+
_logger.LogError(ex, "Error evaluating conditional validation rule for field '{FieldId}'. Skipping rule.", field.FieldId);
174+
continue;
175+
}
133176
}
134177

135178
switch (rule.Type)
@@ -142,26 +185,195 @@ public bool ValidateField(Field field, object value, ModelStateDictionary modelS
142185
}
143186
break;
144187
case "regex":
145-
if (!Regex.IsMatch(stringValue, rule.Rule.ToString(), RegexOptions.None, TimeSpan.FromMilliseconds(200)) && !string.IsNullOrWhiteSpace(stringValue))
188+
var pattern = rule.Rule?.ToString();
189+
if (!string.IsNullOrWhiteSpace(stringValue) && !string.IsNullOrEmpty(pattern))
146190
{
147-
modelState.AddModelError(fieldKey, rule.Message);
148-
isValid = false;
191+
var regexMatch = Regex.IsMatch(stringValue, pattern, RegexOptions.None, TimeSpan.FromMilliseconds(200));
192+
if (!regexMatch)
193+
{
194+
modelState.AddModelError(fieldKey, rule.Message);
195+
isValid = false;
196+
}
149197
}
150198
break;
151199
case "maxLength":
152-
if (stringValue.Length > int.Parse(rule.Rule.ToString()))
200+
var maxLengthStr = rule.Rule?.ToString();
201+
if (!string.IsNullOrEmpty(maxLengthStr) && int.TryParse(maxLengthStr, out var maxLength))
153202
{
154-
modelState.AddModelError(fieldKey, rule.Message);
155-
isValid = false;
203+
if (stringValue.Length > maxLength)
204+
{
205+
modelState.AddModelError(fieldKey, rule.Message);
206+
isValid = false;
207+
}
156208
}
157209
break;
158210
default:
159-
_logger.LogWarning("Unknown validation rule type: {RuleType}", rule.Type);
211+
_logger.LogWarning("Unknown validation rule type: {RuleType} for field '{FieldKey}'", rule.Type, fieldKey);
160212
break;
161213
}
162214
}
163215

164216
return isValid;
165217
}
218+
219+
#region Complex Field Validation
220+
221+
/// <summary>
222+
/// Validates a complex field (upload, autocomplete, etc.)
223+
/// </summary>
224+
/// <param name="field">The complex field to validate</param>
225+
/// <param name="value">The field value</param>
226+
/// <param name="formData">The complete form data for conditional evaluation</param>
227+
/// <param name="modelState">The model state to add errors to</param>
228+
/// <param name="fieldKey">The field key for model state</param>
229+
/// <returns>True if validation passes</returns>
230+
private bool ValidateComplexField(Field field, object? value, Dictionary<string, object>? formData, ModelStateDictionary modelState, string fieldKey)
231+
{
232+
if (field.Validations == null)
233+
{
234+
return true;
235+
}
236+
237+
var stringValue = value?.ToString() ?? string.Empty;
238+
var isValid = true;
239+
240+
// Determine if this is an upload field
241+
bool isUploadField = field.ComplexField!.Id.Contains("Upload", StringComparison.OrdinalIgnoreCase);
242+
243+
foreach (var rule in field.Validations)
244+
{
245+
// Check if this is a conditional validation rule
246+
if (rule.Condition != null)
247+
{
248+
if (formData == null)
249+
{
250+
_logger.LogWarning("Conditional validation rule found for complex field '{FieldId}' but no form data provided for evaluation. Skipping rule.", field.FieldId);
251+
continue;
252+
}
253+
254+
try
255+
{
256+
var conditionResult = _conditionalLogicEngine.EvaluateCondition(rule.Condition, formData);
257+
258+
if (!conditionResult)
259+
{
260+
continue;
261+
}
262+
}
263+
catch (Exception ex)
264+
{
265+
_logger.LogError(ex, "Error evaluating conditional validation rule for complex field '{FieldId}'. Skipping rule.", field.FieldId);
266+
continue;
267+
}
268+
}
269+
270+
switch (rule.Type.ToLowerInvariant())
271+
{
272+
case "required":
273+
if (isUploadField)
274+
{
275+
// For upload fields, check if files are uploaded
276+
bool hasFiles = HasUploadedFiles(stringValue);
277+
if (!hasFiles)
278+
{
279+
// Use a more appropriate error message for upload fields if the template message is clearly wrong
280+
var errorMessage = rule.Message;
281+
if (errorMessage.Contains("phone", StringComparison.OrdinalIgnoreCase) ||
282+
errorMessage.Contains("name", StringComparison.OrdinalIgnoreCase) ||
283+
errorMessage.Contains("text", StringComparison.OrdinalIgnoreCase))
284+
{
285+
errorMessage = "Please upload a file.";
286+
}
287+
288+
modelState.AddModelError(fieldKey, errorMessage);
289+
isValid = false;
290+
}
291+
}
292+
else
293+
{
294+
// For other complex fields (autocomplete), check if value is empty
295+
if (string.IsNullOrWhiteSpace(stringValue))
296+
{
297+
modelState.AddModelError(fieldKey, rule.Message);
298+
isValid = false;
299+
}
300+
}
301+
break;
302+
case "regex":
303+
// Regex validation doesn't apply to upload fields, skip for uploads
304+
if (!isUploadField && !string.IsNullOrWhiteSpace(stringValue))
305+
{
306+
var pattern = rule.Rule?.ToString();
307+
if (!string.IsNullOrEmpty(pattern) && !Regex.IsMatch(stringValue, pattern, RegexOptions.None, TimeSpan.FromMilliseconds(200)))
308+
{
309+
modelState.AddModelError(fieldKey, rule.Message);
310+
isValid = false;
311+
}
312+
}
313+
break;
314+
case "maxlength":
315+
// MaxLength validation doesn't apply to upload fields, skip for uploads
316+
if (!isUploadField)
317+
{
318+
if (int.TryParse(rule.Rule?.ToString(), out var maxLength))
319+
{
320+
if (stringValue.Length > maxLength)
321+
{
322+
modelState.AddModelError(fieldKey, rule.Message);
323+
isValid = false;
324+
}
325+
}
326+
else
327+
{
328+
_logger.LogWarning("Complex field maxLength validation rule has invalid rule value for field '{FieldId}': {Rule}", field.FieldId, rule.Rule);
329+
}
330+
}
331+
break;
332+
default:
333+
_logger.LogWarning("Unknown complex field validation rule type '{Type}' for field '{FieldId}'", rule.Type, field.FieldId);
334+
break;
335+
}
336+
}
337+
338+
return isValid;
339+
}
340+
341+
/// <summary>
342+
/// Checks if an upload field has uploaded files
343+
/// </summary>
344+
/// <param name="value">The field value (JSON array or string)</param>
345+
/// <returns>True if files are uploaded</returns>
346+
private bool HasUploadedFiles(string value)
347+
{
348+
if (string.IsNullOrWhiteSpace(value))
349+
{
350+
return false;
351+
}
352+
353+
// Handle special session data placeholder - this indicates NO files uploaded yet
354+
if (value == "UPLOAD_FIELD_SESSION_DATA")
355+
{
356+
return false;
357+
}
358+
359+
// Try to parse as JSON array
360+
try
361+
{
362+
if (value.StartsWith("[") && value.EndsWith("]"))
363+
{
364+
var files = System.Text.Json.JsonSerializer.Deserialize<List<object>>(value);
365+
return files != null && files.Count > 0;
366+
}
367+
}
368+
catch (Exception ex)
369+
{
370+
_logger.LogWarning(ex, "Failed to parse upload field value as JSON for field value: {Value}", value);
371+
}
372+
373+
// If not JSON or parsing failed, treat non-empty as having files (except for known placeholders)
374+
return !string.IsNullOrWhiteSpace(value);
375+
}
376+
377+
#endregion
166378
}
167379
}

0 commit comments

Comments
 (0)