Skip to content

Commit 688f102

Browse files
Copilotdwarwick
andcommitted
Add validation logic to prevent publishing surveys with orphaned or empty groups
Co-authored-by: dwarwick <15970276+dwarwick@users.noreply.github.com>
1 parent e2b3979 commit 688f102

File tree

5 files changed

+491
-0
lines changed

5 files changed

+491
-0
lines changed

JwtIdentity.Tests/ControllerTests/SurveyControllerTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public override void BaseSetUp()
3636
MockEmailService.Setup(e => e.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(true);
3737
MockConfiguration.Setup(c => c["EmailSettings:CustomerServiceEmail"]).Returns("admin@example.com");
3838
MockSurveyService = new Mock<ISurveyService>();
39+
// Default: validation passes
40+
MockSurveyService.Setup(s => s.ValidateSurveyForPublishingAsync(It.IsAny<int>()))
41+
.ReturnsAsync((true, string.Empty));
3942
var mockQuestionHandlerFactory = new Mock<IQuestionHandlerFactory>();
4043
var mockQuestionHandler = new Mock<IQuestionHandler>();
4144
mockQuestionHandler.Setup(h => h.LoadRelatedDataAsync(It.IsAny<List<int>>(), It.IsAny<ApplicationDbContext>()))
@@ -500,5 +503,100 @@ public async Task PostSurvey_Unauthorized_ReturnsUnauthorized()
500503
// Assert
501504
Assert.That(result.Result, Is.InstanceOf<UnauthorizedResult>());
502505
}
506+
507+
[Test]
508+
public async Task PutSurvey_PublishWithOrphanedGroups_ReturnsBadRequest()
509+
{
510+
// Arrange
511+
MockApiAuthService.Setup(a => a.GetUserId(It.IsAny<ClaimsPrincipal>())).Returns(2);
512+
MockSurveyService.Setup(s => s.ValidateSurveyForPublishingAsync(It.IsAny<int>()))
513+
.ReturnsAsync((false, "Cannot publish survey with unreachable groups: Orphaned Group"));
514+
515+
var surveyVm = new SurveyViewModel
516+
{
517+
Id = 2, // Use survey 2 which is unpublished
518+
Title = "Survey 2",
519+
Description = "Description 2",
520+
Published = true
521+
};
522+
523+
// Act
524+
var result = await _controller.PutSurvey(surveyVm);
525+
526+
// Assert
527+
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
528+
var badRequestResult = result as BadRequestObjectResult;
529+
Assert.That(badRequestResult.Value, Does.Contain("unreachable group"));
530+
}
531+
532+
[Test]
533+
public async Task PutSurvey_PublishWithEmptyGroups_ReturnsBadRequest()
534+
{
535+
// Arrange
536+
MockApiAuthService.Setup(a => a.GetUserId(It.IsAny<ClaimsPrincipal>())).Returns(2);
537+
MockSurveyService.Setup(s => s.ValidateSurveyForPublishingAsync(It.IsAny<int>()))
538+
.ReturnsAsync((false, "Cannot publish survey with empty groups: Empty Group"));
539+
540+
var surveyVm = new SurveyViewModel
541+
{
542+
Id = 2, // Use survey 2 which is unpublished
543+
Title = "Survey 2",
544+
Description = "Description 2",
545+
Published = true
546+
};
547+
548+
// Act
549+
var result = await _controller.PutSurvey(surveyVm);
550+
551+
// Assert
552+
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
553+
var badRequestResult = result as BadRequestObjectResult;
554+
Assert.That(badRequestResult.Value, Does.Contain("empty group"));
555+
}
556+
557+
[Test]
558+
public async Task PutSurvey_PublishWithValidGroups_ReturnsOk()
559+
{
560+
// Arrange
561+
MockApiAuthService.Setup(a => a.GetUserId(It.IsAny<ClaimsPrincipal>())).Returns(2);
562+
MockSurveyService.Setup(s => s.ValidateSurveyForPublishingAsync(It.IsAny<int>()))
563+
.ReturnsAsync((true, string.Empty));
564+
565+
var surveyVm = new SurveyViewModel
566+
{
567+
Id = 2, // Use survey 2 which is unpublished
568+
Title = "Survey 2",
569+
Description = "Description 2",
570+
Published = true
571+
};
572+
573+
// Act
574+
var result = await _controller.PutSurvey(surveyVm);
575+
576+
// Assert
577+
Assert.That(result, Is.InstanceOf<OkObjectResult>());
578+
}
579+
580+
[Test]
581+
public async Task PutSurvey_UpdateWithoutPublishing_DoesNotValidate()
582+
{
583+
// Arrange
584+
MockApiAuthService.Setup(a => a.GetUserId(It.IsAny<ClaimsPrincipal>())).Returns(1);
585+
586+
var surveyVm = new SurveyViewModel
587+
{
588+
Id = 1,
589+
Title = "Updated Survey 1",
590+
Description = "Updated Description 1",
591+
Published = false // Not publishing
592+
};
593+
594+
// Act
595+
var result = await _controller.PutSurvey(surveyVm);
596+
597+
// Assert
598+
Assert.That(result, Is.InstanceOf<OkObjectResult>());
599+
MockSurveyService.Verify(s => s.ValidateSurveyForPublishingAsync(It.IsAny<int>()), Times.Never);
600+
}
503601
}
504602
}

JwtIdentity.Tests/ServiceTests/SurveyServiceTests.cs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Linq;
3+
using System.Threading.Tasks;
4+
using JwtIdentity.Common.Helpers;
35
using JwtIdentity.Interfaces;
46
using JwtIdentity.Models;
57
using JwtIdentity.Services;
@@ -46,5 +48,220 @@ public void GetSurvey_ReturnsNull_WhenGuidDoesNotExist()
4648
var result = _service.GetSurvey("notfound");
4749
Assert.That(result, Is.Null);
4850
}
51+
52+
[Test]
53+
public async Task ValidateSurveyForPublishing_ReturnsValid_WhenNoQuestionGroups()
54+
{
55+
// Arrange
56+
var survey = new Survey
57+
{
58+
Title = "Test Survey",
59+
Description = "Test Description",
60+
Guid = "test-guid",
61+
Published = false,
62+
Questions = new System.Collections.Generic.List<Question>
63+
{
64+
new TextQuestion { Id = 1, Text = "Q1", QuestionNumber = 1, QuestionType = QuestionType.Text, GroupId = 0 }
65+
},
66+
QuestionGroups = new System.Collections.Generic.List<QuestionGroup>()
67+
};
68+
MockDbContext.Surveys.Add(survey);
69+
MockDbContext.SaveChanges();
70+
71+
// Act
72+
var (isValid, errorMessage) = await _service.ValidateSurveyForPublishingAsync(survey.Id);
73+
74+
// Assert
75+
Assert.That(isValid, Is.True);
76+
Assert.That(errorMessage, Is.Empty);
77+
}
78+
79+
[Test]
80+
public async Task ValidateSurveyForPublishing_ReturnsInvalid_WhenGroupIsEmpty()
81+
{
82+
// Arrange
83+
var survey = new Survey
84+
{
85+
Title = "Test Survey",
86+
Description = "Test Description",
87+
Guid = "test-guid",
88+
Published = false,
89+
Questions = new System.Collections.Generic.List<Question>
90+
{
91+
new TextQuestion { Id = 1, Text = "Q1", QuestionNumber = 1, QuestionType = QuestionType.Text, GroupId = 1 }
92+
},
93+
QuestionGroups = new System.Collections.Generic.List<QuestionGroup>
94+
{
95+
new QuestionGroup { Id = 1, SurveyId = 1, GroupNumber = 0, GroupName = "Default" },
96+
new QuestionGroup { Id = 2, SurveyId = 1, GroupNumber = 1, GroupName = "Empty Group" }
97+
}
98+
};
99+
MockDbContext.Surveys.Add(survey);
100+
MockDbContext.SaveChanges();
101+
102+
// Act
103+
var (isValid, errorMessage) = await _service.ValidateSurveyForPublishingAsync(survey.Id);
104+
105+
// Assert
106+
Assert.That(isValid, Is.False);
107+
Assert.That(errorMessage, Does.Contain("empty group").IgnoreCase);
108+
Assert.That(errorMessage, Does.Contain("Empty Group"));
109+
}
110+
111+
[Test]
112+
public async Task ValidateSurveyForPublishing_ReturnsInvalid_WhenGroupIsOrphaned()
113+
{
114+
// Arrange
115+
var survey = new Survey
116+
{
117+
Title = "Test Survey",
118+
Description = "Test Description",
119+
Guid = "test-guid",
120+
Published = false,
121+
Questions = new System.Collections.Generic.List<Question>
122+
{
123+
new TextQuestion { Id = 1, Text = "Q1", QuestionNumber = 1, QuestionType = QuestionType.Text, GroupId = 1 },
124+
new TextQuestion { Id = 2, Text = "Q2", QuestionNumber = 2, QuestionType = QuestionType.Text, GroupId = 2 }
125+
},
126+
QuestionGroups = new System.Collections.Generic.List<QuestionGroup>
127+
{
128+
new QuestionGroup { Id = 1, SurveyId = 1, GroupNumber = 0, GroupName = "Default" },
129+
new QuestionGroup { Id = 2, SurveyId = 1, GroupNumber = 1, GroupName = "Orphaned Group" }
130+
}
131+
};
132+
MockDbContext.Surveys.Add(survey);
133+
MockDbContext.SaveChanges();
134+
135+
// Act
136+
var (isValid, errorMessage) = await _service.ValidateSurveyForPublishingAsync(survey.Id);
137+
138+
// Assert
139+
Assert.That(isValid, Is.False);
140+
Assert.That(errorMessage, Does.Contain("unreachable group").IgnoreCase);
141+
Assert.That(errorMessage, Does.Contain("Orphaned Group"));
142+
}
143+
144+
[Test]
145+
public async Task ValidateSurveyForPublishing_ReturnsValid_WhenGroupConnectedViaNextGroupId()
146+
{
147+
// Arrange
148+
var survey = new Survey
149+
{
150+
Title = "Test Survey",
151+
Description = "Test Description",
152+
Guid = "test-guid",
153+
Published = false,
154+
Questions = new System.Collections.Generic.List<Question>
155+
{
156+
new TextQuestion { Id = 1, Text = "Q1", QuestionNumber = 1, QuestionType = QuestionType.Text, GroupId = 1 },
157+
new TextQuestion { Id = 2, Text = "Q2", QuestionNumber = 2, QuestionType = QuestionType.Text, GroupId = 2 }
158+
},
159+
QuestionGroups = new System.Collections.Generic.List<QuestionGroup>
160+
{
161+
new QuestionGroup { Id = 1, SurveyId = 1, GroupNumber = 0, GroupName = "Default", NextGroupId = 2 },
162+
new QuestionGroup { Id = 2, SurveyId = 1, GroupNumber = 1, GroupName = "Connected Group" }
163+
}
164+
};
165+
MockDbContext.Surveys.Add(survey);
166+
MockDbContext.SaveChanges();
167+
168+
// Act
169+
var (isValid, errorMessage) = await _service.ValidateSurveyForPublishingAsync(survey.Id);
170+
171+
// Assert
172+
Assert.That(isValid, Is.True);
173+
Assert.That(errorMessage, Is.Empty);
174+
}
175+
176+
[Test]
177+
public async Task ValidateSurveyForPublishing_ReturnsValid_WhenGroupConnectedViaTrueFalseBranching()
178+
{
179+
// Arrange
180+
var survey = new Survey
181+
{
182+
Title = "Test Survey",
183+
Description = "Test Description",
184+
Guid = "test-guid",
185+
Published = false,
186+
Questions = new System.Collections.Generic.List<Question>
187+
{
188+
new TrueFalseQuestion
189+
{
190+
Id = 1,
191+
Text = "Q1",
192+
QuestionNumber = 1,
193+
QuestionType = QuestionType.TrueFalse,
194+
GroupId = 1,
195+
BranchToGroupIdOnTrue = 2
196+
},
197+
new TextQuestion { Id = 2, Text = "Q2", QuestionNumber = 2, QuestionType = QuestionType.Text, GroupId = 2 }
198+
},
199+
QuestionGroups = new System.Collections.Generic.List<QuestionGroup>
200+
{
201+
new QuestionGroup { Id = 1, SurveyId = 1, GroupNumber = 0, GroupName = "Default" },
202+
new QuestionGroup { Id = 2, SurveyId = 1, GroupNumber = 1, GroupName = "Connected via True" }
203+
}
204+
};
205+
MockDbContext.Surveys.Add(survey);
206+
MockDbContext.SaveChanges();
207+
208+
// Act
209+
var (isValid, errorMessage) = await _service.ValidateSurveyForPublishingAsync(survey.Id);
210+
211+
// Assert
212+
Assert.That(isValid, Is.True);
213+
Assert.That(errorMessage, Is.Empty);
214+
}
215+
216+
[Test]
217+
public async Task ValidateSurveyForPublishing_ReturnsValid_WhenGroupConnectedViaMultipleChoiceBranching()
218+
{
219+
// Arrange
220+
var mcQuestion = new MultipleChoiceQuestion
221+
{
222+
Id = 1,
223+
Text = "Q1",
224+
QuestionNumber = 1,
225+
QuestionType = QuestionType.MultipleChoice,
226+
GroupId = 1
227+
};
228+
229+
var choiceOption = new ChoiceOption
230+
{
231+
Id = 1,
232+
OptionText = "Option 1",
233+
MultipleChoiceQuestionId = 1,
234+
BranchToGroupId = 2
235+
};
236+
237+
var survey = new Survey
238+
{
239+
Title = "Test Survey",
240+
Description = "Test Description",
241+
Guid = "test-guid",
242+
Published = false,
243+
Questions = new System.Collections.Generic.List<Question>
244+
{
245+
mcQuestion,
246+
new TextQuestion { Id = 2, Text = "Q2", QuestionNumber = 2, QuestionType = QuestionType.Text, GroupId = 2 }
247+
},
248+
QuestionGroups = new System.Collections.Generic.List<QuestionGroup>
249+
{
250+
new QuestionGroup { Id = 1, SurveyId = 1, GroupNumber = 0, GroupName = "Default" },
251+
new QuestionGroup { Id = 2, SurveyId = 1, GroupNumber = 1, GroupName = "Connected via MC" }
252+
}
253+
};
254+
255+
MockDbContext.Surveys.Add(survey);
256+
MockDbContext.ChoiceOptions.Add(choiceOption);
257+
MockDbContext.SaveChanges();
258+
259+
// Act
260+
var (isValid, errorMessage) = await _service.ValidateSurveyForPublishingAsync(survey.Id);
261+
262+
// Assert
263+
Assert.That(isValid, Is.True);
264+
Assert.That(errorMessage, Is.Empty);
265+
}
49266
}
50267
}

JwtIdentity/Controllers/SurveyController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,17 @@ public async Task<IActionResult> PutSurvey(SurveyViewModel surveyViewModel)
512512
var userName = user?.UserName ?? userId.ToString();
513513
var wasPublished = survey.Published;
514514

515+
// Validate survey before publishing
516+
if (surveyViewModel.Published && !wasPublished)
517+
{
518+
var (isValid, errorMessage) = await _surveyService.ValidateSurveyForPublishingAsync(surveyViewModel.Id);
519+
if (!isValid)
520+
{
521+
_logger.LogWarning("Survey {SurveyId} validation failed: {ErrorMessage}", surveyViewModel.Id, errorMessage);
522+
return BadRequest(errorMessage);
523+
}
524+
}
525+
515526
// Update basic properties only
516527
survey.Title = surveyViewModel.Title;
517528
survey.Description = surveyViewModel.Description;

JwtIdentity/Services/ISurveyService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ public interface ISurveyService
44
{
55
Survey GetSurvey(string guid);
66
Task GenerateDemoSurveyResponsesAsync(Survey survey, int numberOfUsers = 20);
7+
Task<(bool IsValid, string ErrorMessage)> ValidateSurveyForPublishingAsync(int surveyId);
78
}
89
}

0 commit comments

Comments
 (0)