diff --git a/pom.xml b/pom.xml index d5e071f9..cf9d3292 100644 --- a/pom.xml +++ b/pom.xml @@ -1,96 +1,153 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.0.6 - - - com.medeiros - SPRINGProject - 0.0.1-SNAPSHOT - SPRINGProject - Demo project for Spring Boot - - 20 - - 6.0.3 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - com.mysql - mysql-connector-j - runtime - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.boot - spring-boot-starter-thymeleaf - 3.0.6 - - - - org.springframework.boot - spring-boot-starter-security - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - - - - - org.springframework.security - spring-security-core - 6.0.3 - - - - - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.0.6 + + + + com.medeiros + SPRINGProject + 0.0.1-SNAPSHOT + SPRINGProject + Demo project for Spring Boot + + 20 + + 6.0.3 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + com.mysql + mysql-connector-j + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + 3.0.6 + + + org.springframework.boot + spring-boot-starter-security + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + org.springframework.security + spring-security-core + 6.0.3 + + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + org.mockito + mockito-junit-jupiter + 2.23.4 + test + + + + io.spring.javaformat + spring-javaformat-formatter + 0.0.40 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + test + + report + + + coverageReport + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.2.5 + + testReport + + + + + org.apache.maven.plugins + maven-site-plugin + 2.1 + + testReport + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + 0.0.40 + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerCommentMusicByIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerCommentMusicByIdTest.java new file mode 100644 index 00000000..f0576ba1 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerCommentMusicByIdTest.java @@ -0,0 +1,582 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=CommentMusicById_73ccb95717 +ROOST_METHOD_SIG_HASH=CommentMusicById_70a5563f90 + +Scenario 1: Successfully increments comment count and saves the updated music + +Details: + TestName: incrementsCommentCountAndPersistsMusic + Description: Validates that when a music record is found, the method increases its number of comments by exactly one and persists the updated entity using MusicRepository.save. + +Execution: + Arrange: + - Create a mock MusicRepository that returns a mock MusicModel when findById(id) is invoked. + - Stub MusicModel.getNumberOfComents() to return a known baseline value (e.g., 5). + - Prepare a mock LogRepository and a mock CommentsRepository. Do not stub any methods on CommentsRepository. + - Construct an instance of AppController wired with the mocked repositories. + Act: + - Call CommentMusicById with the chosen id. + Assert: + - Verify via Mockito that MusicModel.setNumberOfComents(6) is called exactly once (increment by 1). + - Verify that musicRepo.save is invoked exactly once with the same MusicModel instance returned by findById. + Validation: + - Confirms the core business logic: the comment counter is incremented atomically in-memory and persisted. + - Ensures correct repository interaction for updating the domain model. + +Scenario 2: Returns the exact success message in Portuguese + +Details: + TestName: returnsExactPortugueseSuccessMessage + Description: Ensures the method returns the literal success message "Música Alterada!" upon a successful update. + +Execution: + Arrange: + - Mock MusicRepository.findById(id) to return a mock MusicModel with getNumberOfComents() returning any integer. + - Mock LogRepository to accept saves. + - Build AppController with mocks. + Act: + - Capture the returned String from CommentMusicById(id). + Assert: + - Use assertEquals("Música Alterada!", returnedString). + Validation: + - Verifies user-facing feedback is precise and unchanged, which is important for clients relying on exact response text. + +Scenario 3: Logs the action once after saving the music + +Details: + TestName: logsOnceAfterMusicSaved + Description: Confirms that a single log entry is created via LogRepository.save after the music update is persisted. + +Execution: + Arrange: + - Mock MusicRepository to return a MusicModel and allow save. + - Mock LogRepository; no special stubbing needed. + - Build AppController with mocks. + Act: + - Invoke CommentMusicById(id). + Assert: + - Verify that musicRepo.save is called before Log.save (use Mockito InOrder). + - Verify Log.save is called exactly once with any LogModel instance (use ArgumentCaptor or argument matcher). + Validation: + - Ensures that logging is performed as a side-effect of a successful update and that only one log record is persisted. + +Scenario 4: Null music record leads to NullPointerException and prevents save/log + +Details: + TestName: throwsWhenMusicNotFoundById + Description: Ensures that if MusicRepository.findById(id) returns null, the method throws a NullPointerException and does not attempt to save the music or write a log. + +Execution: + Arrange: + - Mock MusicRepository.findById(id) to return null. + - Mock LogRepository. + - Build AppController with mocks. + Act: + - Call CommentMusicById(id) expecting an exception. + Assert: + - Use assertThrows(NullPointerException, () -> controller.CommentMusicById(id)). + - Verify that musicRepo.save is never called. + - Verify that Log.save is never called. + Validation: + - Ensures clear failure behavior when the target music entity is absent and avoids unintended side-effects. + +Scenario 5: Propagates exception when MusicRepository.findById throws + +Details: + TestName: propagatesExceptionFromFindById + Description: Verifies that if MusicRepository.findById throws a runtime exception, the method propagates that exception without saving or logging. + +Execution: + Arrange: + - Stub musicRepo.findById(id) to throw a RuntimeException. + - Mock LogRepository. + - Build AppController with mocks. + Act: + - Invoke CommentMusicById(id) and expect a RuntimeException. + Assert: + - Use assertThrows(RuntimeException, ...). + - Verify no calls to musicRepo.save. + - Verify no calls to Log.save. + Validation: + - Confirms fail-fast behavior and that partial side-effects do not occur when preconditions fail. + +Scenario 6: Propagates exception when MusicRepository.save throws and avoids logging + +Details: + TestName: propagatesWhenMusicSaveFails + Description: Ensures that if persisting the updated music throws an exception, the method propagates the exception and does not attempt to log. + +Execution: + Arrange: + - Mock findById to return a MusicModel with baseline comments. + - Stub musicRepo.save to throw a RuntimeException. + - Mock LogRepository. + - Build AppController. + Act: + - Invoke CommentMusicById(id) expecting a RuntimeException. + Assert: + - assertThrows(RuntimeException, ...). + - Verify that Log.save is never called. + Validation: + - Confirms that logging only happens after a successful persistence and that failures during save halt further processing. + +Scenario 7: Propagates exception when LogRepository.save throws, after music is already saved + +Details: + TestName: propagatesWhenLogSaveFailsAfterMusicSaved + Description: Validates that if logging fails after the music is saved, the exception is propagated and the successful music update remains attempted. + +Execution: + Arrange: + - Mock musicRepo.findById returning a valid MusicModel. + - Allow musicRepo.save to succeed. + - Stub Log.save to throw a RuntimeException. + - Build AppController. + Act: + - Call CommentMusicById(id) expecting a RuntimeException. + Assert: + - assertThrows(RuntimeException, ...). + - Verify musicRepo.save was called exactly once before Log.save. + Validation: + - Ensures correct call ordering and demonstrates partial success (music persisted attempt) with failure during logging being surfaced to the caller. + +Scenario 8: Increments from zero to one + +Details: + TestName: incrementsFromZeroToOne + Description: Checks that starting from zero comments, the updated value passed to setNumberOfComents is exactly one. + +Execution: + Arrange: + - Mock findById to return a MusicModel whose getNumberOfComents() returns 0. + - Mock repositories for save and log. + Act: + - Invoke CommentMusicById(id). + Assert: + - Verify setNumberOfComents(1) is called once on the MusicModel. + Validation: + - Confirms correct handling of the baseline count edge case. + +Scenario 9: Increments a negative comment count by one + +Details: + TestName: incrementsNegativeCommentCount + Description: Ensures that if the current comment count is negative (e.g., -3), the method still increments by one to -2 and persists it. + +Execution: + Arrange: + - Mock findById to return a MusicModel with getNumberOfComents() returning -3. + - Mock repositories for save and log. + Act: + - Invoke CommentMusicById(id). + Assert: + - Verify setNumberOfComents(-2) is called once. + - Verify musicRepo.save is called once with the same MusicModel. + Validation: + - Demonstrates robustness against unexpected negative state by applying a consistent increment rule. + +Scenario 10: Handles integer overflow when starting at Integer.MAX_VALUE + +Details: + TestName: handlesOverflowWhenMaxInt + Description: Validates Java integer overflow behavior: if starting at Integer.MAX_VALUE, incrementing results in Integer.MIN_VALUE being passed to setNumberOfComents. + +Execution: + Arrange: + - Mock findById to return a MusicModel whose getNumberOfComents() returns Integer.MAX_VALUE. + - Mock repositories for save and log. + Act: + - Call CommentMusicById(id). + Assert: + - Verify setNumberOfComents(Integer.MIN_VALUE) is called once. + - Verify musicRepo.save called once with the MusicModel. + Validation: + - Captures edge-of-range behavior, important for data integrity and potential overflow handling strategies. + +Scenario 11: Accepts and processes a negative id when a record exists + +Details: + TestName: processesNegativeIdWhenRecordExists + Description: Ensures that a negative id parameter is accepted and processed if MusicRepository.findById returns a valid MusicModel. + +Execution: + Arrange: + - Choose a negative id (e.g., -42). + - Mock findById(-42) to return a MusicModel with getNumberOfComents() returning a known value. + - Mock repositories for save and log. + Act: + - Invoke CommentMusicById(-42). + Assert: + - Verify the increment and save as in the happy path. + - Assert the returned string equals "Música Alterada!". + Validation: + - Confirms the method does not validate id sign and relies on repository behavior, which can be intentional in some systems. + +Scenario 12: Accepts and processes id zero when a record exists + +Details: + TestName: processesZeroIdWhenRecordExists + Description: Ensures that id = 0 is accepted and processed if the repository finds a corresponding MusicModel. + +Execution: + Arrange: + - Mock findById(0) to return a MusicModel with a known comment count. + - Mock repositories for save and log. + Act: + - Invoke CommentMusicById(0). + Assert: + - Verify setNumberOfComents is called with count + 1 and that save and log each occur once. + Validation: + - Verifies the method’s neutrality to specific id values where repository policy allows such ids. + +Scenario 13: No interactions with CommentsRepository or algorithmBlender in this method + +Details: + TestName: noUnrelatedDependenciesInvoked + Description: Confirms that CommentMusicById only uses MusicRepository and LogRepository, leaving CommentsRepository and algorithmBlender unused. + +Execution: + Arrange: + - Prepare mocks for MusicRepository, LogRepository, and CommentsRepository. + - Build AppController with these mocks and a default algorithmBlender instance. + Act: + - Invoke CommentMusicById(id). + Assert: + - Verify no interactions with CommentsRepository. + - (Optionally) Ensure no external algorithmBlender behavior is invoked by confirming only the expected interactions occur on repositories. + Validation: + - Guards against unintended side-effects or tight coupling with unrelated components. + +Scenario 14: Two sequential invocations increment comments twice and log twice + +Details: + TestName: twoSequentialCallsIncrementTwiceAndLogTwice + Description: Ensures idempotent per-call behavior: two direct calls result in two increments and two log entries. + +Execution: + Arrange: + - Mock MusicRepository.findById(id) to return the same MusicModel mock each time with getNumberOfComents() initially returning 10, then 11 on subsequent read if you model state; or simply verify the set calls. + - Mock repositories for save and log. + Act: + - Call CommentMusicById(id) twice sequentially. + Assert: + - Verify setNumberOfComents(11) and then setNumberOfComents(12) are called in sequence (use InOrder). + - Verify musicRepo.save is called twice. + - Verify Log.save is called twice. + Validation: + - Confirms that the method can be called repeatedly and that each invocation performs a distinct increment and log. + +Scenario 15: Verifies call order within a single invocation + +Details: + TestName: verifiesFindIncrementSaveThenLogOrder + Description: Checks the internal order of interactions: find, read current count, set incremented count, persist, then log. + +Execution: + Arrange: + - Mock MusicRepository and LogRepository. + - Configure MusicModel as in the happy path. + Act: + - Invoke CommentMusicById(id). + Assert: + - Use Mockito InOrder to assert the following relative order: + 1) musicRepo.findById(id) + 2) music.getNumberOfComents() + 3) music.setNumberOfComents(current + 1) + 4) musicRepo.save(music) + 5) Log.save(any LogModel) + Validation: + - Ensures a logical sequence that first updates domain state, persists it, and only then logs the action. + +Scenario 16: Demonstrates potential lost update under concurrent-like setup + +Details: + TestName: demonstratesLostUpdateRiskWithSameBaseline + Description: Simulates two invocations that both read the same baseline count to illustrate a potential lost update (both set to the same incremented value). + +Execution: + Arrange: + - Mock MusicRepository.findById(id) to always return a MusicModel whose getNumberOfComents() returns the same fixed value (e.g., 5) each time. + - Mock repositories for save and log. + Act: + - Call CommentMusicById(id) twice sequentially without altering the mock’s returned count between calls. + Assert: + - Verify that setNumberOfComents(6) is invoked twice (indicating both increments used the same baseline). + - Verify that musicRepo.save is called twice and Log.save is called twice. + Validation: + - Highlights the read-modify-write pattern’s susceptibility to lost updates without additional concurrency control, important for understanding system behavior under race conditions. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.CommentsModel; +import com.medeiros.SPRINGProject.Models.CommentsRepository; +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.MusicModel; +import com.medeiros.SPRINGProject.Models.MusicRepository; +import com.medeiros.SPRINGProject.algorithm.algorithmBlender; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.*; + +@ExtendWith(MockitoExtension.class) +public class AppControllerCommentMusicByIdTest { + + @Mock + private MusicRepository musicRepo; + + @Mock + private LogRepository Log; + + @Mock + private CommentsRepository CommentsRepo; + + @Mock + private MusicModel music; + + @InjectMocks + @Autowired // Annotation present for demonstration purposes in test context + private AppController controller; + + @Test + @Tag("valid") + public void testIncrementsCommentCountAndPersistsMusic() { + int id = 123; // TODO: Replace with appropriate id if needed + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(5); + String result = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(6); + verify(musicRepo, times(1)).save(music); + assertEquals((String) "Música Alterada!", (String) result); + } + + @Test + @Tag("valid") + public void testReturnsExactPortugueseSuccessMessage() { + int id = 1; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(0); + String returnedString = controller.CommentMusicById(id); + assertEquals((String) "Música Alterada!", (String) returnedString); + } + + @Test + @Tag("valid") + public void testLogsOnceAfterMusicSaved() { + int id = 77; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(2); + String r = controller.CommentMusicById(id); + InOrder inOrder = inOrder(musicRepo, Log); + inOrder.verify(musicRepo, times(1)).save(music); + ArgumentCaptor captor = ArgumentCaptor.forClass(LogModel.class); + inOrder.verify(Log, times(1)).save(captor.capture()); + LogModel savedLog = captor.getValue(); + assertNotNull((Object) savedLog); + assertEquals((String) "Música Alterada!", (String) r); + } + + @Test + @Tag("invalid") + public void testThrowsWhenMusicNotFoundById() { + int id = 9999; // TODO: Replace with expected absent id if needed + when(musicRepo.findById(id)).thenReturn(null); + assertThrows(NullPointerException.class, () -> controller.CommentMusicById(id)); + verify(musicRepo, never()).save(any(MusicModel.class)); + verify(Log, never()).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testPropagatesExceptionFromFindById() { + int id = 22; + when(musicRepo.findById(id)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> controller.CommentMusicById(id)); + verify(musicRepo, never()).save(any(MusicModel.class)); + verify(Log, never()).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testPropagatesWhenMusicSaveFails() { + int id = 42; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(3); + when(musicRepo.save(music)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> controller.CommentMusicById(id)); + verify(Log, never()).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testPropagatesWhenLogSaveFailsAfterMusicSaved() { + int id = 55; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(8); + doThrow(new RuntimeException()).when(Log).save(any(LogModel.class)); + assertThrows(RuntimeException.class, () -> controller.CommentMusicById(id)); + InOrder inOrder = inOrder(musicRepo, Log); + inOrder.verify(musicRepo, times(1)).save(music); + inOrder.verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testIncrementsFromZeroToOne() { + int id = 2; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(0); + String ret = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(1); + assertEquals((String) "Música Alterada!", (String) ret); + } + + @Test + @Tag("boundary") + public void testIncrementsNegativeCommentCount() { + int id = 3; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(-3); + String r = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(-2); + verify(musicRepo, times(1)).save(music); + assertEquals((String) "Música Alterada!", (String) r); + } + + @Test + @Tag("boundary") + public void testHandlesOverflowWhenMaxInt() { + int id = 4; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(Integer.MAX_VALUE); + String r = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(Integer.MIN_VALUE); + verify(musicRepo, times(1)).save(music); + assertEquals((String) "Música Alterada!", (String) r); + } + + @Test + @Tag("boundary") + public void testProcessesNegativeIdWhenRecordExists() { + int id = -42; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(12); + String r = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(13); + verify(musicRepo, times(1)).save(music); + assertEquals((String) "Música Alterada!", (String) r); + } + + @Test + @Tag("boundary") + public void testProcessesZeroIdWhenRecordExists() { + int id = 0; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(7); + String r = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(8); + verify(musicRepo, times(1)).save(music); + verify(Log, times(1)).save(any(LogModel.class)); + assertEquals((String) "Música Alterada!", (String) r); + } + + @Test + @Tag("valid") + public void testNoUnrelatedDependenciesInvoked() { + int id = 9; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(1); + String r = controller.CommentMusicById(id); + verifyNoInteractions(CommentsRepo); + assertEquals((String) "Música Alterada!", (String) r); + } + + @Test + @Tag("integration") + public void testTwoSequentialCallsIncrementTwiceAndLogTwice() { + int id = 11; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(10, 11); + String r1 = controller.CommentMusicById(id); + String r2 = controller.CommentMusicById(id); + InOrder inOrder = inOrder(music, musicRepo, Log); + inOrder.verify(music, times(1)).getNumberOfComents(); + inOrder.verify(music, times(1)).setNumberOfComents(11); + inOrder.verify(musicRepo, times(1)).save(music); + inOrder.verify(Log, times(1)).save(any(LogModel.class)); + inOrder.verify(music, times(1)).getNumberOfComents(); + inOrder.verify(music, times(1)).setNumberOfComents(12); + inOrder.verify(musicRepo, times(1)).save(music); + inOrder.verify(Log, times(1)).save(any(LogModel.class)); + assertEquals((String) "Música Alterada!", (String) r1); + assertEquals((String) "Música Alterada!", (String) r2); + } + + @Test + @Tag("valid") + public void testVerifiesFindIncrementSaveThenLogOrder() { + int id = 18; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(5); + String res = controller.CommentMusicById(id); + InOrder inOrder = inOrder(musicRepo, music, Log); + inOrder.verify(musicRepo, times(1)).findById(id); + inOrder.verify(music, times(1)).getNumberOfComents(); + inOrder.verify(music, times(1)).setNumberOfComents(6); + inOrder.verify(musicRepo, times(1)).save(music); + inOrder.verify(Log, times(1)).save(any(LogModel.class)); + assertEquals((String) "Música Alterada!", (String) res); + } + + @Test + @Tag("integration") + public void testDemonstratesLostUpdateRiskWithSameBaseline() { + int id = 25; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(5, 5); + String r1 = controller.CommentMusicById(id); + String r2 = controller.CommentMusicById(id); + verify(music, times(2)).setNumberOfComents(6); + verify(musicRepo, times(2)).save(music); + verify(Log, times(2)).save(any(LogModel.class)); + assertEquals((String) "Música Alterada!", (String) r1); + assertEquals((String) "Música Alterada!", (String) r2); + } + + @Test + @Tag("valid") + public void testAlgorithmBlenderInstantiationNotAffectingThisMethod() { + // Ensuring the imported class is utilized without affecting logic. + algorithmBlender blender = new algorithmBlender(); + assertNotNull((Object) blender); + int id = 5; + when(musicRepo.findById(id)).thenReturn(music); + when(music.getNumberOfComents()).thenReturn(4); + String result = controller.CommentMusicById(id); + verify(music, times(1)).setNumberOfComents(5); + verify(musicRepo, times(1)).save(music); + verify(Log, times(1)).save(any(LogModel.class)); + assertEquals((String) "Música Alterada!", (String) result); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerCreateCommentTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerCreateCommentTest.java new file mode 100644 index 00000000..9ec19440 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerCreateCommentTest.java @@ -0,0 +1,124 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=createComment_e2e99943cb +ROOST_METHOD_SIG_HASH=createComment_4104bef389 + +Scenario 1: Successfully creates and logs a comment with a valid id and non-empty text + +Details: + TestName: createsAndLogsCommentWithValidInputs + Description: Verifies that when provided with a valid music id and a non-empty comment string, the method creates a CommentsModel, saves it via CommentsRepository, logs the operation via LogRepository, and returns the original comment string. + +Execution: + Arrange: Set up an AppController instance. Inject a mock CommentsRepository into the controller’s CommentsRepo field and a mock LogRepository into the controller’s Log field. Optionally spy or stub the Date field (LogModel) to return a fixed time from getTimeNow(), if feasible. Prepare a valid id (e.g., 42) and a comment string (e.g., "Great track!"). + Act: Invoke createComment with the prepared id and comment string. + Assert: + - Assert that the returned value equals the same "Great track!" string. + - Verify CommentsRepo.save is called exactly once with any CommentsModel instance. + - Verify Log.save is called exactly once with any LogModel instance. + +Validation: + Confirms the core flow is executed: a comment is persisted, an audit log entry is written, and the original input comment is returned unmodified. This demonstrates the happy-path behavior and the integration with both persistence and logging components. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.junit.jupiter.api.Assertions.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.algorithm.algorithmBlender; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +@ExtendWith(MockitoExtension.class) +public class AppControllerCreateCommentTest { + + @InjectMocks + private AppController controller; + + @Mock + private CommentsRepository CommentsRepo; + + @Mock + private LogRepository Log; + + @Test + @Tag("integration") + public void testCreatesAndLogsCommentWithValidInputs() { + int id = 42; // TODO change to a valid id if needed + String inputComment = "Great track!"; + String result = controller.createComment(id, inputComment); + assertEquals((String) inputComment, (String) result); + verify(CommentsRepo, times(1)).save(any(CommentsModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + verifyNoMoreInteractions(CommentsRepo, Log); + } + + @Test + @Tag("boundary") + public void testCreatesCommentWithEmptyText() { + int id = 10; // TODO change to a valid id if needed + String inputComment = ""; + String result = controller.createComment(id, inputComment); + assertEquals((String) inputComment, (String) result); + verify(CommentsRepo, times(1)).save(any(CommentsModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + verifyNoMoreInteractions(CommentsRepo, Log); + } + + @Test + @Tag("boundary") + public void testCreatesCommentWithVeryLongText() { + int id = 7; // TODO change to a valid id if needed + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 5000; i++) { + sb.append('a'); + } + String inputComment = sb.toString(); + String result = controller.createComment(id, inputComment); + assertEquals((String) inputComment, (String) result); + verify(CommentsRepo, times(1)).save(any(CommentsModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + verifyNoMoreInteractions(CommentsRepo, Log); + } + + @Test + @Tag("invalid") + public void testThrowsWhenCommentsRepoSaveFails() { + int id = 15; // TODO change to a valid id if needed + String inputComment = "Will fail on repo save"; + doThrow(new RuntimeException("Simulated persistence failure")).when(CommentsRepo) + .save(any(CommentsModel.class)); + assertThrows(RuntimeException.class, () -> controller.createComment(id, inputComment)); + verify(CommentsRepo, times(1)).save(any(CommentsModel.class)); + verify(Log, times(0)).save(any(LogModel.class)); + verifyNoMoreInteractions(CommentsRepo, Log); + } + + @Test + @Tag("invalid") + public void testThrowsWhenLogSaveFails() { + int id = 21; // TODO change to a valid id if needed + String inputComment = "Will fail on log save"; + doThrow(new RuntimeException("Simulated log failure")).when(Log).save(any(LogModel.class)); + assertThrows(RuntimeException.class, () -> controller.createComment(id, inputComment)); + verify(CommentsRepo, times(1)).save(any(CommentsModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + verifyNoMoreInteractions(CommentsRepo, Log); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerGetCommentsByMusicIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerGetCommentsByMusicIdTest.java new file mode 100644 index 00000000..42f8971b --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerGetCommentsByMusicIdTest.java @@ -0,0 +1,76 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=getCommentsByMusicId_9981fa8dac +ROOST_METHOD_SIG_HASH=getCommentsByMusicId_b64d712e58 + +Scenario 1: Returns constant "a" for a typical positive MusicId + +Details: + TestName: returnsConstantAForValidMusicId + Description: Verify that calling AppController.getCommentsByMusicId with a normal positive integer returns the constant string "a", as implemented. + +Execution: + Arrange: Instantiate AppController. Do not inject any dependencies since the method does not use them. + Act: Invoke getCommentsByMusicId with a MusicId such as 42. + Assert: Use JUnit assertions to confirm the result equals "a". + +Validation: + Confirming the exact constant output ensures the current implementation is honored and helps detect unintended changes to the method’s return value in future refactoring. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.*; +import com.medeiros.SPRINGProject.algorithm.algorithmBlender; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +@ExtendWith(MockitoExtension.class) +public class AppControllerGetCommentsByMusicIdTest { + + @Test + @Tag("valid") + public void returnsConstantAForValidMusicId() { + AppController controller = new AppController(); + String result = controller.getCommentsByMusicId(42); + assertEquals((String) "a", (String) result); + } + + @Test + @Tag("boundary") + public void returnsConstantAForZeroMusicId() { + AppController controller = new AppController(); + String result = controller.getCommentsByMusicId(0); + assertEquals((String) "a", (String) result); + } + + @Test + @Tag("invalid") + public void returnsConstantAForNegativeMusicId() { + AppController controller = new AppController(); + String result = controller.getCommentsByMusicId(-1); + assertEquals((String) "a", (String) result); + } + + @Test + @Tag("boundary") + public void returnsConstantAForMaxIntMusicId() { + AppController controller = new AppController(); + String result = controller.getCommentsByMusicId(Integer.MAX_VALUE); + assertEquals((String) "a", (String) result); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerLikeMusicByIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerLikeMusicByIdTest.java new file mode 100644 index 00000000..406c161c --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerLikeMusicByIdTest.java @@ -0,0 +1,141 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=LikeMusicById_9bce296f63 +ROOST_METHOD_SIG_HASH=LikeMusicById_86ac50a5c5 + +Scenario 1: Increments likes and returns success message for a valid music id + +Details: + TestName: incrementsLikesAndReturnsSuccessMessage + Description: Verifies the standard success path where a music record is found by id, its like count is incremented by one, the updated entity is saved, a log entry is created, and the method returns the expected success String. + +Execution: + Arrange: Configure MusicRepository to return a mock MusicModel for a given id. Stub getNumberOfLikes() to return a known value (e.g., 10). Prepare LogRepository as a mock to accept a LogModel. + Act: Invoke LikeMusicById with the configured id. + Assert: + - Assert that the returned value equals "Música Alterada!". + - Verify that setNumberOfLikes(11) is called once on the MusicModel (original 10 + 1). + - Verify that musicRepo.save is invoked once with the same MusicModel instance. + - Verify that Log.save is invoked once with a non-null LogModel instance. + +Validation: + This confirms that the method performs the core business logic: increments likes by exactly one, persists the change, logs the action, and returns the expected confirmation message. It ensures the happy path works end-to-end. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.MusicModel; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.ArgumentCaptor; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.*; +import com.medeiros.SPRINGProject.algorithm.algorithmBlender; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Map; + +@ExtendWith(MockitoExtension.class) +public class AppControllerLikeMusicByIdTest { + + @Mock + private MusicRepository musicRepo; + + @Mock + private LogRepository Log; + + @InjectMocks + private AppController appController; + + @Test + @Tag("valid") + public void testIncrementsLikesAndReturnsSuccessMessage() { + int id = 1; // TODO: adjust id if needed for a different test setup + MusicModel musicMock = mock(MusicModel.class); + when(musicRepo.findById(id)).thenReturn(musicMock); + when(musicMock.getNumberOfLikes()).thenReturn(10); + String result = appController.LikeMusicById(id); + assertEquals((String) "Música Alterada!", (String) result); + verify(musicMock, times(1)).setNumberOfLikes(11); + verify(musicRepo, times(1)).save(musicMock); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(Log, times(1)).save(logCaptor.capture()); + assertNotNull((Object) logCaptor.getValue()); + verify(musicRepo, times(1)).findById(id); + verifyNoMoreInteractions(musicRepo, Log, musicMock); + } + + @Test + @Tag("invalid") + public void testThrowsWhenMusicNotFound() { + int id = 100; // TODO: adjust id to a non-existent id if test data changes + when(musicRepo.findById(id)).thenReturn(null); + assertThrows(NullPointerException.class, () -> appController.LikeMusicById(id)); + verify(musicRepo, times(1)).findById(id); + verify(musicRepo, never()).save(any(MusicModel.class)); + verify(Log, never()).save(any(LogModel.class)); + verifyNoMoreInteractions(musicRepo, Log); + } + + @Test + @Tag("boundary") + public void testBoundaryWhenLikesAtMaxInt() { + int id = 2; + MusicModel musicMock = mock(MusicModel.class); + when(musicRepo.findById(id)).thenReturn(musicMock); + when(musicMock.getNumberOfLikes()).thenReturn(Integer.MAX_VALUE); + String result = appController.LikeMusicById(id); + int expectedOverflowValue = (int) ((long) Integer.MAX_VALUE + 1); + assertEquals((String) "Música Alterada!", (String) result); + verify(musicMock, times(1)).setNumberOfLikes(expectedOverflowValue); + verify(musicRepo, times(1)).save(musicMock); + verify(Log, times(1)).save(any(LogModel.class)); + verifyNoMoreInteractions(musicRepo, Log, musicMock); + } + + @Test + @Tag("invalid") + public void testPropagatesWhenRepositorySaveThrows() { + int id = 3; + MusicModel musicMock = mock(MusicModel.class); + when(musicRepo.findById(id)).thenReturn(musicMock); + when(musicMock.getNumberOfLikes()).thenReturn(5); + doThrow(new RuntimeException()).when(musicRepo).save(musicMock); + assertThrows(RuntimeException.class, () -> appController.LikeMusicById(id)); + verify(musicMock, times(1)).setNumberOfLikes(6); + verify(musicRepo, times(1)).save(musicMock); + verify(Log, never()).save(any(LogModel.class)); + verifyNoMoreInteractions(musicRepo, Log, musicMock); + } + + @Test + @Tag("invalid") + public void testPropagatesWhenLogSaveThrows() { + int id = 4; + MusicModel musicMock = mock(MusicModel.class); + when(musicRepo.findById(id)).thenReturn(musicMock); + when(musicMock.getNumberOfLikes()).thenReturn(0); + doThrow(new RuntimeException()).when(Log).save(any(LogModel.class)); + assertThrows(RuntimeException.class, () -> appController.LikeMusicById(id)); + verify(musicMock, times(1)).setNumberOfLikes(1); + verify(musicRepo, times(1)).save(musicMock); + verify(Log, times(1)).save(any(LogModel.class)); + verifyNoMoreInteractions(musicRepo, Log, musicMock); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerShowMusicsTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerShowMusicsTest.java new file mode 100644 index 00000000..0131ca79 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AppControllerShowMusicsTest.java @@ -0,0 +1,125 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=showMusics_52ef65b78a +ROOST_METHOD_SIG_HASH=showMusics_caba7c787a + +Scenario 1: Returns the same Map produced by algorithmBlender when repository provides typical data + +Details: + TestName: returnsAlgorithmMapWhenRepoHasData + Description: Verifies that showMusics delegates to MusicRepository.findAll(), forwards the resulting Iterable to algorithmBlender.algorithmCalc, and returns exactly the Map provided by algorithmBlender without alteration. + +Execution: + Arrange: Set up a mock MusicRepository that returns a non-empty Iterable. Prepare a mock algorithmBlender configured to return a predetermined Map. Replace the ab field in an AppController instance with this mock (e.g., via reflection), and inject the mocked MusicRepository into the controller instance. + Act: Invoke showMusics() on the controller. + Assert: Use assertions to confirm that the returned Map is the exact same instance as the Map produced by the mock algorithmBlender. Verify the repository’s findAll() was called once and algorithmBlender.algorithmCalc(...) was invoked exactly once with the Iterable returned by findAll(). + +Validation: + This validates correct orchestration: the method must call repository.findAll(), pass the result to algorithmBlender, and return the algorithm’s output unchanged. It ensures no unintended transformations occur in AppController, which is essential for correct separation of concerns. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.MusicModel; +import com.medeiros.SPRINGProject.Models.MusicRepository; +import com.medeiros.SPRINGProject.algorithm.algorithmBlender; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class AppControllerShowMusicsTest { + + @Mock + private MusicRepository musicRepo; + + @Mock + private algorithmBlender ab; + + @InjectMocks + private AppController controller; + + @Test + @Tag("valid") + public void testReturnsAlgorithmMapWhenRepoHasData() { + Iterable listFromRepo = Arrays.asList(Mockito.mock(MusicModel.class), + Mockito.mock(MusicModel.class)); + Mockito.when(musicRepo.findAll()).thenReturn(listFromRepo); + Map expectedMap = new HashMap<>(); + Mockito.when(ab.algorithmCalc(Mockito.same(listFromRepo))).thenReturn(expectedMap); + Map actualMap = controller.showMusics(); + Assertions.assertSame((Map) expectedMap, (Map) actualMap); + Mockito.verify(musicRepo, Mockito.times(1)).findAll(); + Mockito.verify(ab, Mockito.times(1)).algorithmCalc(Mockito.same(listFromRepo)); + Mockito.verifyNoMoreInteractions(musicRepo, ab); + } + + @Test + @Tag("boundary") + public void testReturnsSameMapWhenRepoIsEmpty() { + Iterable emptyList = Collections.emptyList(); + Mockito.when(musicRepo.findAll()).thenReturn(emptyList); + Map expectedMap = new HashMap<>(); + Mockito.when(ab.algorithmCalc(Mockito.same(emptyList))).thenReturn(expectedMap); + Map actualMap = controller.showMusics(); + Assertions.assertSame((Map) expectedMap, (Map) actualMap); + Mockito.verify(musicRepo, Mockito.times(1)).findAll(); + Mockito.verify(ab, Mockito.times(1)).algorithmCalc(Mockito.same(emptyList)); + Mockito.verifyNoMoreInteractions(musicRepo, ab); + } + + @Test + @Tag("invalid") + public void testReturnsNullWhenAlgorithmReturnsNull() { + Iterable listFromRepo = Arrays.asList(Mockito.mock(MusicModel.class)); + Mockito.when(musicRepo.findAll()).thenReturn(listFromRepo); + Mockito.when(ab.algorithmCalc(Mockito.same(listFromRepo))).thenReturn(null); + Map actualMap = controller.showMusics(); + Assertions.assertNull((Map) actualMap); + Mockito.verify(musicRepo, Mockito.times(1)).findAll(); + Mockito.verify(ab, Mockito.times(1)).algorithmCalc(Mockito.same(listFromRepo)); + Mockito.verifyNoMoreInteractions(musicRepo, ab); + } + + @Test + @Tag("invalid") + public void testPropagatesExceptionWhenRepoThrows() { + Mockito.when(musicRepo.findAll()).thenThrow(new RuntimeException()); + Assertions.assertThrows(RuntimeException.class, () -> controller.showMusics()); + Mockito.verify(musicRepo, Mockito.times(1)).findAll(); + Mockito.verifyNoInteractions(ab); + Mockito.verifyNoMoreInteractions(musicRepo); + } + + @Test + @Tag("invalid") + public void testPropagatesExceptionWhenAlgorithmThrows() { + Iterable listFromRepo = Arrays.asList(Mockito.mock(MusicModel.class)); + Mockito.when(musicRepo.findAll()).thenReturn(listFromRepo); + Mockito.when(ab.algorithmCalc(Mockito.same(listFromRepo))).thenThrow(new RuntimeException()); + Assertions.assertThrows(RuntimeException.class, () -> controller.showMusics()); + Mockito.verify(musicRepo, Mockito.times(1)).findAll(); + Mockito.verify(ab, Mockito.times(1)).algorithmCalc(Mockito.same(listFromRepo)); + Mockito.verifyNoMoreInteractions(musicRepo, ab); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AuthControllerCreateAuserTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AuthControllerCreateAuserTest.java new file mode 100644 index 00000000..5d32825f --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AuthControllerCreateAuserTest.java @@ -0,0 +1,623 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=createAuser_43c81bbbba +ROOST_METHOD_SIG_HASH=createAuser_2684b4910b + +Scenario 1: Mismatched passwords should return the mismatch message and avoid persistence + +Details: + TestName: returnsMismatchMessageWhenPasswordsDiffer + Description: Verifies that AuthController.createAuser returns "Senhas não batem!" when password and confirmpassword differ, and ensures no user credentials are saved and no log entry is written. + +Execution: + Arrange: Create an instance of AuthController. Provide mocked UserAccRepository and LogRepository. Leave other unused dependencies as mocks or null as appropriate. + Act: Call createAuser with email="user@example.com", password="abc123", confirmpassword="ABC123", username="tester". + Assert: Assert the returned string equals "Senhas não batem!". Verify that UserAccRepo.save is never called and Log.save is never called. + +Validation: + Confirms that the guard clause using Objects.equals enforces exact equality and prevents side effects (no save or log) when passwords do not match. This preserves data integrity by not creating inconsistent user records. + + +Scenario 2: Matching passwords should return success and persist both user and log + +Details: + TestName: returnsSuccessAndPersistsWhenPasswordsMatch + Description: Ensures that when password equals confirmpassword, AuthController.createAuser returns "Sucesso", saves a User_Credentials to UserAccRepo, and saves a corresponding LogModel with the expected values to Log. + +Execution: + Arrange: Create AuthController with mocked UserAccRepository and LogRepository. Replace the Date field (LogModel instance) with a spy or stub that returns a fixed timestamp (e.g., "2026-01-01T00:00:00Z") from getTimeNow (use reflection to set the private field if needed). + Act: Call createAuser with email="user@example.com", password="abc123", confirmpassword="abc123", username="tester". + Assert: Assert the returned string equals "Sucesso". Verify UserAccRepo.save is called exactly once with a User_Credentials constructed from the provided email, password, and username. Capture the argument passed to Log.save and verify it is a LogModel created with action="createUser01", entity="User", and the timestamp returned by Date.getTimeNow. + +Validation: + Confirms the happy path behavior: user creation is attempted and a log entry is written with the exact action and entity labels and the controller’s timestamp source. This validates both the result message and critical side effects. + + +Scenario 3: Both password and confirmpassword null should be treated as equal and succeed + +Details: + TestName: allowsNullPasswordsWhenBothNull + Description: Validates that Objects.equals treats two null values as equal, allowing the method to proceed with saving and logging, returning "Sucesso". + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Spy/stub the Date.getTimeNow to return a fixed value. + Act: Call createAuser with email="user@example.com", password=null, confirmpassword=null, username="tester". + Assert: Assert the returned string equals "Sucesso". Verify UserAccRepo.save and Log.save are each called exactly once. + +Validation: + Ensures the method relies strictly on Objects.equals semantics for equality, and does not block creation when both password fields are null. This documents current behavior for null handling. + + +Scenario 4: One null password and one non-null should return the mismatch message + +Details: + TestName: returnsMismatchWhenOnePasswordIsNull + Description: Checks that when one of the password values is null and the other is not, the method returns "Senhas não batem!" and performs no persistence. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. + Act: Call createAuser with email="user@example.com", password=null, confirmpassword="abc123", username="tester" (and vice versa in a separate assertion if desired). + Assert: Assert the returned string equals "Senhas não batem!". Verify no calls to UserAccRepo.save or Log.save. + +Validation: + Verifies robust mismatch detection for asymmetric null values, preventing unintended user creation. + + +Scenario 5: Equal empty-string passwords should succeed + +Details: + TestName: allowsEmptyPasswordsWhenBothEmpty + Description: Ensures that empty strings are considered equal when both password and confirmpassword are "", resulting in a successful save-and-log and "Sucesso" return. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Stub Date.getTimeNow. + Act: Call createAuser with email="user@example.com", password="", confirmpassword="", username="tester". + Assert: Assert the returned string equals "Sucesso". Verify UserAccRepo.save and Log.save are called exactly once each. + +Validation: + Documents that the controller applies no non-emptiness validation to passwords and proceeds based purely on equality. + + +Scenario 6: Case-sensitive mismatch in passwords should fail + +Details: + TestName: treatsCaseDifferenceAsMismatch + Description: Verifies that password comparison is case-sensitive by returning "Senhas não batem!" when values differ by case only. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. + Act: Call createAuser with password="Password123" and confirmpassword="password123". + Assert: Assert the returned string equals "Senhas não batem!". Verify no calls to UserAccRepo.save or Log.save. + +Validation: + Clarifies that exact string equality (including case) is required, which is important for security expectations. + + +Scenario 7: Null email with matching passwords should still return success and persist + +Details: + TestName: savesEvenWhenEmailIsNullIfPasswordsMatch + Description: Confirms that the controller does not validate the email value; if passwords match, it returns "Sucesso" and performs save and log operations. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Stub Date.getTimeNow. + Act: Call createAuser with email=null, password="abc123", confirmpassword="abc123", username="tester". + Assert: Assert the returned string equals "Sucesso". Verify single calls to UserAccRepo.save and Log.save. + +Validation: + Highlights that input validation for email is not enforced by the controller. This scenario captures the current behavior for upstream/downstream validation considerations. + + +Scenario 8: Null username with matching passwords should still return success and persist + +Details: + TestName: savesEvenWhenUsernameIsNullIfPasswordsMatch + Description: Ensures the controller does not validate username; with matching passwords, it proceeds to save and log, returning "Sucesso". + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Stub Date.getTimeNow. + Act: Call createAuser with email="user@example.com", password="abc123", confirmpassword="abc123", username=null. + Assert: Assert the returned string equals "Sucesso". Verify calls to UserAccRepo.save and Log.save occur exactly once each. + +Validation: + Documents the absence of username validation and that persistence proceeds solely on password equality. + + +Scenario 9: Exception during user save should propagate and prevent logging + +Details: + TestName: propagatesExceptionWhenUserSaveFails + Description: Validates that if UserAccRepo.save throws a runtime exception, the method does not catch it, does not attempt to log, and propagates the exception. + +Execution: + Arrange: Mock UserAccRepository to throw a RuntimeException on save. Mock LogRepository. Stub Date.getTimeNow if needed. + Act: Invoke createAuser with matching passwords and expect an exception. + Assert: Assert that a RuntimeException is thrown. Verify Log.save is never called. + +Validation: + Confirms lack of error-handling within the method and ensures logging is not attempted after a failed persistence operation, preserving consistency. + + +Scenario 10: Exception during log save should propagate after successful user save + +Details: + TestName: propagatesExceptionWhenLogSaveFails + Description: Ensures that if Log.save throws a runtime exception, the exception is propagated and the method does not return "Sucesso", while confirming the user was saved before the log attempt. + +Execution: + Arrange: Mock UserAccRepository to save without error. Mock LogRepository to throw RuntimeException on save. Stub Date.getTimeNow. + Act: Call createAuser with matching passwords and expect an exception. + Assert: Assert that a RuntimeException is thrown. Verify UserAccRepo.save is called once and Log.save is called once. + +Validation: + Verifies operation order and error propagation: user persistence happens before logging, and a logging failure interrupts the response. + + +Scenario 11: Ensure user is saved before log entry is written + +Details: + TestName: savesUserBeforeWritingLog + Description: Confirms the sequencing of operations: save to UserAccRepo occurs before Log.save. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Use an interaction sequence verifier (e.g., in-order verification). + Act: Call createAuser with matching passwords. + Assert: Verify that the call to UserAccRepo.save occurs before the call to Log.save. + +Validation: + Ensures the intended order of side effects, which can be important for audit trails and consistency. + + +Scenario 12: Timestamp is obtained once from the controller’s Date field on success + +Details: + TestName: requestsTimestampFromDateOnceOnSuccess + Description: Validates that Date.getTimeNow is invoked exactly once when the success path is taken. + +Execution: + Arrange: Replace the Date field with a spy/stubbed LogModel whose getTimeNow returns a fixed value and can be verified for invocation count. + Act: Call createAuser with matching passwords. + Assert: Verify getTimeNow is invoked exactly once. + +Validation: + Confirms that the controller fetches the timestamp a single time per operation, aligning with performance and correctness expectations. + + +Scenario 13: Unused dependencies are not interacted with during create + +Details: + TestName: doesNotUseUnusedDependenciesDuringCreate + Description: Ensures that JwtUtil and UserInfoRepository are not used by createAuser. + +Execution: + Arrange: Provide mocks for JwtUtil and UserInfoRepository (if injected or set), alongside necessary mocks for UserAccRepository and LogRepository. + Act: Call createAuser with matching passwords. + Assert: Verify there are no interactions with JwtUtil and UserInfoRepo. + +Validation: + Documents that createAuser is isolated to user credential persistence and logging, helping prevent unintended coupling. + + +Scenario 14: Repeated calls with identical inputs consistently succeed and trigger repeated persistence + +Details: + TestName: returnsSuccessOnRepeatedCallsWithSameInputs + Description: Verifies idempotent response (not idempotent side effects) with repeated identical inputs: each call returns "Sucesso" and triggers its own save and log. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Stub Date.getTimeNow to return deterministic or sequential values if needed. + Act: Call createAuser twice with the same email, password, confirmpassword, and username. + Assert: Assert both calls return "Sucesso". Verify UserAccRepo.save is called twice and Log.save is called twice. + +Validation: + Shows that the controller does not enforce uniqueness or deduplication and that each call results in persistence and logging. + + +Scenario 15: Accepts very long input strings when passwords match + +Details: + TestName: acceptsVeryLongInputsWhenPasswordsMatch + Description: Ensures the method handles large input sizes without additional validation, returning "Sucesso" and performing persistence and logging. + +Execution: + Arrange: Construct very long strings for email, password, and username; ensure password equals confirmpassword. Mock UserAccRepository and LogRepository. + Act: Call createAuser with the long inputs. + Assert: Assert the returned string equals "Sucesso". Verify UserAccRepo.save and Log.save are called exactly once each. + +Validation: + Demonstrates that the controller imposes no length constraints and relies on downstream components for such validation. + + +Scenario 16: Leading/trailing whitespace in equal passwords still succeeds + +Details: + TestName: treatsWhitespaceAsSignificantInPasswordMatching + Description: Confirms that the controller does not trim passwords; equal strings with whitespace pass and result in success. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. + Act: Call createAuser with password=" pass " and confirmpassword=" pass ". + Assert: Assert the returned string equals "Sucesso". Verify UserAccRepo.save and Log.save each invoked once. + +Validation: + Captures current behavior that whitespace is treated as part of the password, important for avoiding silent data transformations. + + +Scenario 17: Special characters in credentials are accepted when passwords match + +Details: + TestName: acceptsSpecialCharactersWhenPasswordsMatch + Description: Ensures the method handles special characters (e.g., symbols, Unicode) in email, password, and username, provided passwords match. + +Execution: + Arrange: Mock UserAccRepository and LogRepository. Stub Date.getTimeNow. + Act: Call createAuser with email="üser+alias@example-domain.com", password="p@$$w0rd!✓", confirmpassword="p@$$w0rd!✓", username="téßt_user-✓". + Assert: Assert the returned string equals "Sucesso". Verify one call to UserAccRepo.save and one call to Log.save. + +Validation: + Demonstrates that the controller does not constrain character sets and properly delegates to persistence and logging when passwords match. + + +Scenario 18: Log content uses exact action and entity constants + +Details: + TestName: writesLogWithExpectedActionAndEntity + Description: Focuses on verifying that the created log entry uses the exact strings "createUser01" for action and "User" for entity, alongside the timestamp from Date.getTimeNow. + +Execution: + Arrange: Mock LogRepository with an argument captor for LogModel. Replace Date with a spy/stub returning a known timestamp. Mock UserAccRepository to accept saves. + Act: Call createAuser with matching passwords. + Assert: Capture the LogModel passed to Log.save and verify it was created with action="createUser01", entity="User", and the stubbed timestamp. + +Validation: + Ensures audit log semantics are precise and unchanged, which is critical for traceability and monitoring. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.UserAccRepository; +import com.medeiros.SPRINGProject.Models.UserInfoRepository; +import com.medeiros.SPRINGProject.Models.User_Credentials; +import com.medeiros.SPRINGProject.Security.JwtUtil; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import java.lang.reflect.Field; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Objects; + +@ExtendWith(MockitoExtension.class) +public class AuthControllerCreateAuserTest { + + @Mock + private JwtUtil jwtUtil; + + @Mock + private UserAccRepository userAccRepository; + + @Mock + private UserInfoRepository userInfoRepository; + + @Mock + private LogRepository logRepository; + + private static void setPrivateField(Object target, String fieldName, Object value) { + try { + Field f = target.getClass().getDeclaredField(fieldName); + f.setAccessible(true); + f.set(target, value); + } + catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private AuthController buildController(JwtUtil jwt, UserAccRepository accRepo, UserInfoRepository infoRepo, + LogRepository logRepo, LogModel dateObj) { + AuthController controller = new AuthController(); + setPrivateField(controller, "JwtToken", jwt); + setPrivateField(controller, "UserAccRepo", accRepo); + setPrivateField(controller, "UserInfoRepo", infoRepo); + setPrivateField(controller, "Log", logRepo); + if (dateObj != null) { + setPrivateField(controller, "Date", dateObj); + } + return controller; + } + + private static String buildLongString(int length, char c) { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(c); + } + return sb.toString(); + } + + @Test + @Tag("invalid") + public void returnsMismatchMessageWhenPasswordsDiffer() { + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + null); + String result = controller.createAuser("user@example.com", "abc123", "ABC123", "tester"); + assertEquals((String) "Senhas não batem!", (String) result); + verify(userAccRepository, never()).save(any(User_Credentials.class)); + verify(logRepository, never()).save(any(LogModel.class)); + } + + @Test + @Tag("valid") + public void returnsSuccessAndPersistsWhenPasswordsMatch() { + LogModel dateSpy = spy(new LogModel()); + // TODO: Adjust the fixed timestamp if the application time format changes + String fixedTimestamp = "2026-01-01T00:00:00Z"; + doReturn(fixedTimestamp).when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(logRepository, times(1)).save(logCaptor.capture()); + LogModel savedLog = logCaptor.getValue(); + // Ensure a LogModel is provided + assertEquals((Class) LogModel.class, (Class) savedLog.getClass()); + verify(dateSpy, times(1)).getTimeNow(); + } + + @Test + @Tag("boundary") + public void allowsNullPasswordsWhenBothNull() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", null, null, "tester"); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void returnsMismatchWhenOnePasswordIsNull() { + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + null); + String result1 = controller.createAuser("user@example.com", null, "abc123", "tester"); + String result2 = controller.createAuser("user@example.com", "abc123", null, "tester"); + assertEquals((String) "Senhas não batem!", (String) result1); + assertEquals((String) "Senhas não batem!", (String) result2); + verify(userAccRepository, never()).save(any(User_Credentials.class)); + verify(logRepository, never()).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void allowsEmptyPasswordsWhenBothEmpty() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "", "", "tester"); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void treatsCaseDifferenceAsMismatch() { + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + null); + String result = controller.createAuser("user@example.com", "Password123", "password123", "tester"); + assertEquals((String) "Senhas não batem!", (String) result); + verify(userAccRepository, never()).save(any(User_Credentials.class)); + verify(logRepository, never()).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void savesEvenWhenEmailIsNullIfPasswordsMatch() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser(null, "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void savesEvenWhenUsernameIsNullIfPasswordsMatch() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "abc123", "abc123", null); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("integration") + public void propagatesExceptionWhenUserSaveFails() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + when(userAccRepository.save(any(User_Credentials.class))).thenThrow(new RuntimeException("User save failed")); + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> controller.createAuser("user@example.com", "abc123", "abc123", "tester")); + assertEquals((String) "User save failed", (String) thrown.getMessage()); + verify(logRepository, never()).save(any(LogModel.class)); + verify(dateSpy, never()).getTimeNow(); + } + + @Test + @Tag("integration") + public void propagatesExceptionWhenLogSaveFails() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + when(logRepository.save(any(LogModel.class))).thenThrow(new RuntimeException("Log save failed")); + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> controller.createAuser("user@example.com", "abc123", "abc123", "tester")); + assertEquals((String) "Log save failed", (String) thrown.getMessage()); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + verify(dateSpy, times(1)).getTimeNow(); + } + + @Test + @Tag("integration") + public void savesUserBeforeWritingLog() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) result); + InOrder inOrder = inOrder(userAccRepository, logRepository); + inOrder.verify(userAccRepository).save(any(User_Credentials.class)); + inOrder.verify(logRepository).save(any(LogModel.class)); + } + + @Test + @Tag("integration") + public void requestsTimestampFromDateOnceOnSuccess() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) result); + verify(dateSpy, times(1)).getTimeNow(); + } + + @Test + @Tag("integration") + public void doesNotUseUnusedDependenciesDuringCreate() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) result); + verifyNoInteractions(jwtUtil); + verifyNoInteractions(userInfoRepository); + } + + @Test + @Tag("valid") + public void returnsSuccessOnRepeatedCallsWithSameInputs() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String first = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + String second = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) first); + assertEquals((String) "Sucesso", (String) second); + verify(userAccRepository, times(2)).save(any(User_Credentials.class)); + verify(logRepository, times(2)).save(any(LogModel.class)); + verify(dateSpy, times(2)).getTimeNow(); + } + + @Test + @Tag("boundary") + public void acceptsVeryLongInputsWhenPasswordsMatch() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String longEmail = buildLongString(5000, 'e') + "@example.com"; + String longPassword = buildLongString(10000, 'p'); + String longUsername = buildLongString(5000, 'u'); + String result = controller.createAuser(longEmail, longPassword, longPassword, longUsername); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void treatsWhitespaceAsSignificantInPasswordMatching() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", " pass ", " pass ", "tester"); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void acceptsSpecialCharactersWhenPasswordsMatch() { + LogModel dateSpy = spy(new LogModel()); + doReturn("2026-01-01T00:00:00Z").when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String email = "üser+alias@example-domain.com"; + String password = "p@$w0rd!✓"; + String username = "téßt_user-✓"; + String result = controller.createAuser(email, password, password, username); + assertEquals((String) "Sucesso", (String) result); + verify(userAccRepository, times(1)).save(any(User_Credentials.class)); + verify(logRepository, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("integration") + public void writesLogWithExpectedActionAndEntity() { + LogModel dateSpy = spy(new LogModel()); + String fixedTimestamp = "2026-01-01T00:00:00Z"; // TODO: Update if the timestamp + // format changes in the system + doReturn(fixedTimestamp).when(dateSpy).getTimeNow(); + AuthController controller = buildController(jwtUtil, userAccRepository, userInfoRepository, logRepository, + dateSpy); + String result = controller.createAuser("user@example.com", "abc123", "abc123", "tester"); + assertEquals((String) "Sucesso", (String) result); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(logRepository, times(1)).save(logCaptor.capture()); + LogModel savedLog = logCaptor.getValue(); + assertEquals((Class) LogModel.class, (Class) savedLog.getClass()); + verify(dateSpy, times(1)).getTimeNow(); + InOrder order = inOrder(dateSpy, logRepository); + order.verify(dateSpy).getTimeNow(); + order.verify(logRepository).save(any(LogModel.class)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/AuthControllerLoginUserTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/AuthControllerLoginUserTest.java new file mode 100644 index 00000000..1b34afdb --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/AuthControllerLoginUserTest.java @@ -0,0 +1,663 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=loginUser_d19666d97a +ROOST_METHOD_SIG_HASH=loginUser_8fe76c816e + +Scenario 1: Successful login when email exists and password matches exactly + +Details: + TestName: successfulLoginReturnsLogado + Description: Verifies that when a user is found by email and the provided password matches the stored password exactly, the method returns the success message "Logado". + +Execution: + Arrange: Set up an AuthController instance with UserAccRepo injected as a mock. Configure the mock so that UserAccRepo.findUserByEmail("user@example.com") returns a User_Credentials constructed with email "user@example.com", password "P@ssw0rd", and a username (any valid string). + Act: Invoke loginUser("user@example.com", "P@ssw0rd"). + Assert: Assert that the returned value equals "Logado". + +Validation: + Confirming the return value verifies the happy-path behavior: the method checks the repository result, compares passwords using Objects.equals, and returns the exact success string when credentials match. This ensures the core login success flow behaves as intended. + + +Scenario 2: Incorrect password for existing email + +Details: + TestName: wrongPasswordReturnsSenhaIncorreta + Description: Ensures that when a user is found by email but the provided password does not match the stored password, the method returns "Senha incorreta". + +Execution: + Arrange: Inject a mock UserAccRepo into AuthController. Configure UserAccRepo.findUserByEmail("user@example.com") to return a User_Credentials with password "Correct123". + Act: Call loginUser("user@example.com", "Wrong123"). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + The assertion verifies that the method correctly detects a password mismatch via Objects.equals and responds with the predefined incorrect password message, reflecting expected authentication failure behavior. + + +Scenario 3: Email not found in repository + +Details: + TestName: unknownEmailReturnsEmailNaoEncontrado + Description: Validates that when the repository returns null for a provided email, the method responds with "Email não encontrado!". + +Execution: + Arrange: Mock UserAccRepo in AuthController so that UserAccRepo.findUserByEmail("missing@example.com") returns null. + Act: Call loginUser("missing@example.com", "anyPassword"). + Assert: Assert that the returned value equals "Email não encontrado!". + +Validation: + This confirms the method’s null-check branch and ensures that unknown accounts produce the exact "Email não encontrado!" message, aligning with user feedback expectations. + + +Scenario 4: Stored and provided passwords are both null + +Details: + TestName: nullStoredAndNullProvidedPasswordReturnsLogado + Description: Checks the edge case where the stored password is null and the provided password is also null. Because Objects.equals(null, null) is true, the method should return "Logado". + +Execution: + Arrange: Configure UserAccRepo.findUserByEmail("user@example.com") to return a User_Credentials whose password is null. + Act: Invoke loginUser("user@example.com", null). + Assert: Assert that the returned value equals "Logado". + +Validation: + This tests how Objects.equals handles null values. It verifies that a null stored password is treated as equal to a null provided password, producing a successful login outcome per the current logic. + + +Scenario 5: Stored password is null but provided password is non-null + +Details: + TestName: nullStoredPasswordWithNonNullProvidedReturnsSenhaIncorreta + Description: Ensures that when the stored password is null and the provided password is a non-null value, the method returns "Senha incorreta". + +Execution: + Arrange: Set up UserAccRepo to return a User_Credentials with password null for "user@example.com". + Act: Call loginUser("user@example.com", "NonNullPassword"). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + Confirms that Objects.equals(null, "something") evaluates to false and that the method returns the incorrect password message in this mixed null case. + + +Scenario 6: Stored password is non-null but provided password is null + +Details: + TestName: nonNullStoredPasswordWithNullProvidedReturnsSenhaIncorreta + Description: Validates that when the stored password has a value and the provided password is null, the method returns "Senha incorreta". + +Execution: + Arrange: Configure UserAccRepo to return a User_Credentials with password "abc123" for "user@example.com". + Act: Invoke loginUser("user@example.com", null). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + Ensures that a null provided password does not match a non-null stored password and the expected failure message is returned. + + +Scenario 7: Empty string password matches empty string stored + +Details: + TestName: emptyPasswordMatchingStoredEmptyReturnsLogado + Description: Tests that when the stored password is an empty string and the provided password is also an empty string, the method returns "Logado". + +Execution: + Arrange: Mock UserAccRepo so that it returns a User_Credentials with password "" for "user@example.com". + Act: Call loginUser("user@example.com", ""). + Assert: Assert that the returned value equals "Logado". + +Validation: + Verifies that empty strings are treated as valid values and that equality on empty strings results in a successful login response. + + +Scenario 8: Provided empty string password does not match non-empty stored password + +Details: + TestName: emptyPasswordAgainstNonEmptyStoredReturnsSenhaIncorreta + Description: Ensures that an empty provided password is considered incorrect when the stored password is non-empty. + +Execution: + Arrange: Configure UserAccRepo to return a User_Credentials with password "nonEmpty" for "user@example.com". + Act: Invoke loginUser("user@example.com", ""). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + Confirms that empty input does not falsely authenticate against a non-empty stored password. + + +Scenario 9: Null email leads to not found result + +Details: + TestName: nullEmailReturnsEmailNaoEncontrado + Description: Validates behavior when the provided email is null and the repository does not find a user (returns null), expecting "Email não encontrado!". + +Execution: + Arrange: Mock UserAccRepo so that UserAccRepo.findUserByEmail(null) returns null. + Act: Call loginUser(null, "anyPassword"). + Assert: Assert that the returned value equals "Email não encontrado!". + +Validation: + Ensures the method safely handles null email parameters by relying on repository output and returning the not-found message when no user is found. + + +Scenario 10: Empty or whitespace-only email results in not found + +Details: + TestName: whitespaceEmailReturnsEmailNaoEncontrado + Description: Tests that an empty or whitespace-only email does not match any user and yields "Email não encontrado!". + +Execution: + Arrange: Configure UserAccRepo to return null for UserAccRepo.findUserByEmail(" "). + Act: Invoke loginUser(" ", "password"). + Assert: Assert that the returned value equals "Email não encontrado!". + +Validation: + This scenario confirms that the method does not trim or alter input and depends strictly on the repository to find the user. Without a match, it properly returns the not-found message. + + +Scenario 11: Email case sensitivity results in not found when case differs + +Details: + TestName: emailCaseDifferenceReturnsEmailNaoEncontrado + Description: Ensures that if the repository is case-sensitive and the provided email uses different casing, the user is not found and the method returns "Email não encontrado!". + +Execution: + Arrange: Mock UserAccRepo so that UserAccRepo.findUserByEmail("User@Example.com") returns null, while the stored user would exist for "user@example.com" (but that method is not invoked in this test). + Act: Call loginUser("User@Example.com", "P@ssw0rd"). + Assert: Assert that the returned value equals "Email não encontrado!". + +Validation: + Confirms that no normalization is performed by the controller and that lookups rely on exact matches as provided by the repository outcome. + + +Scenario 12: Password case sensitivity yields incorrect password + +Details: + TestName: passwordCaseDifferenceReturnsSenhaIncorreta + Description: Validates that a password with different letter casing does not match and results in "Senha incorreta". + +Execution: + Arrange: Configure UserAccRepo to return a User_Credentials with password "SecretPass" for "user@example.com". + Act: Invoke loginUser("user@example.com", "secretpass"). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + Demonstrates that password comparisons are case-sensitive and exact, matching Objects.equals behavior. + + +Scenario 13: Password with trailing whitespace does not match + +Details: + TestName: trailingWhitespaceInPasswordReturnsSenhaIncorreta + Description: Checks that a provided password with trailing whitespace is not considered equal to a stored password without whitespace. + +Execution: + Arrange: Mock UserAccRepo to return a User_Credentials with password "pass123" for "user@example.com". + Act: Call loginUser("user@example.com", "pass123 "). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + Ensures no trimming or normalization is applied to the password, preserving strict equality behavior. + + +Scenario 14: International characters in credentials with exact match succeed + +Details: + TestName: unicodeCredentialsMatchingReturnLogado + Description: Verifies that credentials containing non-ASCII characters (e.g., accents, emojis) authenticate successfully when they match exactly. + +Execution: + Arrange: Set UserAccRepo to return a User_Credentials with email "usér@exámple.com" and password "päss🔒". + Act: Invoke loginUser("usér@exámple.com", "päss🔒"). + Assert: Assert that the returned value equals "Logado". + +Validation: + Confirms the method relies on standard Java string equality, which supports Unicode, enabling correct handling of internationalized credentials. + + +Scenario 15: Extremely long password matches successfully + +Details: + TestName: longPasswordMatchingReturnsLogado + Description: Ensures that very long passwords are handled by the method and, when matching exactly, result in "Logado". + +Execution: + Arrange: Configure UserAccRepo to return a User_Credentials with a very long password string (e.g., thousands of characters) for "user@example.com". + Act: Call loginUser("user@example.com", the same long password). + Assert: Assert that the returned value equals "Logado". + +Validation: + Validates correct behavior with large input sizes and that equality checks do not truncate or alter long strings. + + +Scenario 16: SQL injection-like strings do not authenticate if not found + +Details: + TestName: sqlInjectionLikeEmailReturnsEmailNaoEncontrado + Description: Verifies that an email resembling an SQL injection payload does not authenticate when the repository does not find a matching user. + +Execution: + Arrange: Mock UserAccRepo so that UserAccRepo.findUserByEmail("user@example.com' OR '1'='1") returns null. + Act: Invoke loginUser("user@example.com' OR '1'='1", "irrelevant"). + Assert: Assert that the returned value equals "Email não encontrado!". + +Validation: + Demonstrates that input is treated as a literal string, and without a repository match, the not-found message is returned. It highlights that sanitization is not performed at this layer. + + +Scenario 17: Repository throws a runtime exception and method propagates it + +Details: + TestName: repositoryFailurePropagatesException + Description: Ensures that if UserAccRepo.findUserByEmail throws a runtime exception (e.g., due to a database failure), the method does not catch it and the exception propagates. + +Execution: + Arrange: Configure UserAccRepo mock so that calling findUserByEmail("user@example.com") throws a RuntimeException. + Act: Invoke loginUser("user@example.com", "anyPassword") and capture the thrown exception. + Assert: Assert that a RuntimeException is thrown. + +Validation: + Confirms the method contains no exception handling around repository calls and validates the behavior under infrastructure failures, aiding in understanding error propagation. + + +Scenario 18: Uninitialized repository causes a NullPointerException + +Details: + TestName: nullRepositoryReferenceCausesNullPointerException + Description: Verifies that if AuthController.UserAccRepo is not initialized (null), invoking loginUser results in a NullPointerException. + +Execution: + Arrange: Create an AuthController instance without injecting UserAccRepo (leave it null). + Act: Call loginUser("user@example.com", "password") and observe the outcome. + Assert: Assert that a NullPointerException is thrown. + +Validation: + Highlights the importance of dependency injection and proper setup for the controller, reflecting expected failure when required dependencies are missing. + + +Scenario 19: Multiple consecutive successful calls return consistent results + +Details: + TestName: repeatedSuccessfulCallsReturnLogadoConsistently + Description: Ensures that calling loginUser multiple times with the same valid credentials consistently returns "Logado", indicating stateless behavior for this method. + +Execution: + Arrange: Mock UserAccRepo to return a User_Credentials with password "Again123" for "user@example.com". + Act: Call loginUser("user@example.com", "Again123") twice in succession. + Assert: Assert that both calls return "Logado". + +Validation: + Confirms consistent, deterministic behavior across repeated invocations and that no hidden state affects outcomes. + + +Scenario 20: Whitespace-only password matches stored whitespace-only password + +Details: + TestName: whitespaceOnlyPasswordMatchingReturnsLogado + Description: Validates that a password consisting solely of whitespace characters matches if the stored password is identical whitespace. + +Execution: + Arrange: Configure UserAccRepo to return a User_Credentials with password " " (three spaces) for "user@example.com". + Act: Invoke loginUser("user@example.com", " "). + Assert: Assert that the returned value equals "Logado". + +Validation: + Confirms that the method uses literal string equality without trimming and that whitespace-only values are treated as valid matching strings. + + +Scenario 21: Email with leading/trailing spaces does not match if repository expects exact match + +Details: + TestName: emailWithSurroundingSpacesReturnsEmailNaoEncontrado + Description: Checks that an email with leading or trailing spaces will not find a user when the repository expects exact, untrimmed input, resulting in "Email não encontrado!". + +Execution: + Arrange: Mock UserAccRepo so that UserAccRepo.findUserByEmail(" user@example.com ") returns null. + Act: Call loginUser(" user@example.com ", "P@ssw0rd"). + Assert: Assert that the returned value equals "Email não encontrado!". + +Validation: + Verifies no trimming is applied to input and underscores the need for the caller or earlier layers to normalize inputs if desired. + + +Scenario 22: Null email with repository returning a user still respects password check + +Details: + TestName: nullEmailWithUserFoundAndMatchingPasswordReturnsLogado + Description: Tests a corner case where the repository returns a user even when a null email is provided, and the provided password matches the stored password, leading to "Logado". + +Execution: + Arrange: Configure UserAccRepo so that UserAccRepo.findUserByEmail(null) returns a User_Credentials with password "MatchMe". + Act: Invoke loginUser(null, "MatchMe"). + Assert: Assert that the returned value equals "Logado". + +Validation: + Demonstrates that the method’s logic relies solely on the repository output and password equality, irrespective of email nullity once a user object is obtained. + + +Scenario 23: Provided password with internal whitespace differs from stored without whitespace + +Details: + TestName: internalWhitespaceInPasswordReturnsSenhaIncorreta + Description: Ensures that a password containing internal whitespace characters does not match a stored password that lacks those characters, resulting in "Senha incorreta". + +Execution: + Arrange: Mock UserAccRepo to return a User_Credentials with password "pass123" for "user@example.com". + Act: Call loginUser("user@example.com", "pass 123"). + Assert: Assert that the returned value equals "Senha incorreta". + +Validation: + Confirms strict character-by-character comparison, reinforcing that any deviation—even internal spaces—causes authentication to fail. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import com.medeiros.SPRINGProject.Security.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.*; + +@ExtendWith(MockitoExtension.class) +class AuthControllerLoginUserTest { + + @Mock + private UserAccRepository UserAccRepo; + + @InjectMocks + private AuthController authController; + + @Test + @Tag("valid") + @DisplayName("Scenario 1: Successful login when email exists and password matches exactly") + public void successfulLoginReturnsLogado() { + String email = "user@example.com"; + String password = "P@ssw0rd"; + User_Credentials user = new User_Credentials(email, password, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, password); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 2: Incorrect password for existing email") + public void wrongPasswordReturnsSenhaIncorreta() { + String email = "user@example.com"; + String storedPassword = "Correct123"; + String providedPassword = "Wrong123"; + User_Credentials user = new User_Credentials(email, storedPassword, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, providedPassword); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 3: Email not found in repository") + public void unknownEmailReturnsEmailNaoEncontrado() { + String email = "missing@example.com"; + when(UserAccRepo.findUserByEmail(email)).thenReturn(null); + String result = authController.loginUser(email, "anyPassword"); + assertEquals((String) "Email não encontrado!", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 4: Stored and provided passwords are both null") + public void nullStoredAndNullProvidedPasswordReturnsLogado() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, null, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, null); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 5: Stored password is null but provided is non-null") + public void nullStoredPasswordWithNonNullProvidedReturnsSenhaIncorreta() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, null, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, "NonNullPassword"); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 6: Stored non-null password but provided is null") + public void nonNullStoredPasswordWithNullProvidedReturnsSenhaIncorreta() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, "abc123", "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, null); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 7: Empty string password matches empty stored password") + public void emptyPasswordMatchingStoredEmptyReturnsLogado() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, "", "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, ""); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 8: Provided empty password does not match non-empty stored") + public void emptyPasswordAgainstNonEmptyStoredReturnsSenhaIncorreta() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, "nonEmpty", "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, ""); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 9: Null email leads to not found result") + public void nullEmailReturnsEmailNaoEncontrado() { + when(UserAccRepo.findUserByEmail(null)).thenReturn(null); + String result = authController.loginUser(null, "anyPassword"); + assertEquals((String) "Email não encontrado!", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(null); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 10: Whitespace-only email results in not found") + public void whitespaceEmailReturnsEmailNaoEncontrado() { + String email = " "; + when(UserAccRepo.findUserByEmail(email)).thenReturn(null); + String result = authController.loginUser(email, "password"); + assertEquals((String) "Email não encontrado!", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 11: Email case difference results in not found") + public void emailCaseDifferenceReturnsEmailNaoEncontrado() { + String email = "User@Example.com"; + when(UserAccRepo.findUserByEmail(email)).thenReturn(null); + String result = authController.loginUser(email, "P@ssw0rd"); + assertEquals((String) "Email não encontrado!", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 12: Password case sensitivity yields incorrect password") + public void passwordCaseDifferenceReturnsSenhaIncorreta() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, "SecretPass", "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, "secretpass"); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 13: Trailing whitespace in password returns incorrect") + public void trailingWhitespaceInPasswordReturnsSenhaIncorreta() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, "pass123", "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, "pass123 "); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 14: International characters with exact match succeed") + public void unicodeCredentialsMatchingReturnLogado() { + String email = "usér@exámple.com"; + String password = "päss🔒"; + User_Credentials user = new User_Credentials(email, password, "nome"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, password); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 15: Extremely long password matches successfully") + public void longPasswordMatchingReturnsLogado() { + String email = "user@example.com"; + StringBuilder sb = new StringBuilder(); + // TODO adjust long password length if needed + for (int i = 0; i < 5000; i++) { + sb.append('A'); + } + String longPassword = sb.toString(); + User_Credentials user = new User_Credentials(email, longPassword, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, longPassword); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 16: SQL injection-like email does not authenticate if not found") + public void sqlInjectionLikeEmailReturnsEmailNaoEncontrado() { + String email = "user@example.com' OR '1'='1"; + when(UserAccRepo.findUserByEmail(email)).thenReturn(null); + String result = authController.loginUser(email, "irrelevant"); + assertEquals((String) "Email não encontrado!", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("integration") + @DisplayName("Scenario 17: Repository throws a runtime exception and method propagates it") + public void repositoryFailurePropagatesException() { + String email = "user@example.com"; + when(UserAccRepo.findUserByEmail(email)).thenThrow(new RuntimeException("DB failure")); + RuntimeException ex = assertThrows(RuntimeException.class, + () -> authController.loginUser(email, "anyPassword")); + assertNotNull((Object) ex); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("integration") + @DisplayName("Scenario 18: Uninitialized repository causes a NullPointerException") + public void nullRepositoryReferenceCausesNullPointerException() { + AuthController controllerWithoutRepo = new AuthController(); + assertThrows(NullPointerException.class, () -> controllerWithoutRepo.loginUser("user@example.com", "password")); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 19: Multiple consecutive successful calls return consistent results") + public void repeatedSuccessfulCallsReturnLogadoConsistently() { + String email = "user@example.com"; + String password = "Again123"; + User_Credentials user = new User_Credentials(email, password, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result1 = authController.loginUser(email, password); + String result2 = authController.loginUser(email, password); + assertEquals((String) "Logado", (String) result1); + assertEquals((String) "Logado", (String) result2); + verify(UserAccRepo, times(2)).findUserByEmail(email); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 20: Whitespace-only password matches stored whitespace-only password") + public void whitespaceOnlyPasswordMatchingReturnsLogado() { + String email = "user@example.com"; + String spacePwd = " "; + User_Credentials user = new User_Credentials(email, spacePwd, "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, spacePwd); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 21: Email with leading/trailing spaces results in not found") + public void emailWithSurroundingSpacesReturnsEmailNaoEncontrado() { + String email = " user@example.com "; + when(UserAccRepo.findUserByEmail(email)).thenReturn(null); + String result = authController.loginUser(email, "P@ssw0rd"); + assertEquals((String) "Email não encontrado!", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 22: Null email with user found and matching password returns success") + public void nullEmailWithUserFoundAndMatchingPasswordReturnsLogado() { + String password = "MatchMe"; + User_Credentials user = new User_Credentials(null, password, "username"); + when(UserAccRepo.findUserByEmail(null)).thenReturn(user); + String result = authController.loginUser(null, password); + assertEquals((String) "Logado", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(null); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 23: Provided password with internal whitespace differs from stored") + public void internalWhitespaceInPasswordReturnsSenhaIncorreta() { + String email = "user@example.com"; + User_Credentials user = new User_Credentials(email, "pass123", "username"); + when(UserAccRepo.findUserByEmail(email)).thenReturn(user); + String result = authController.loginUser(email, "pass 123"); + assertEquals((String) "Senha incorreta", (String) result); + verify(UserAccRepo, times(1)).findUserByEmail(email); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerCreateForumIndexTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerCreateForumIndexTest.java new file mode 100644 index 00000000..64d3bf82 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerCreateForumIndexTest.java @@ -0,0 +1,100 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=createForumIndex_fec126ecbb +ROOST_METHOD_SIG_HASH=createForumIndex_d46662bc78 + +Scenario 1: Returns success message and persists forum with typical valid inputs + +Details: + TestName: returnsSuccessMessageAndPersistsForumWhenValidInput + Description: Validates that when provided with a non-empty name, a non-empty description, and a positive userId, the method constructs a forum entry, persists it via ForumIndexRepository.save, and returns the fixed success message. + +Execution: + Arrange: Set up a ForumController instance with ForumIndexRepository mocked to accept any ForumIndexModel; ensure LogRepository and ForumChatRepository are also mocked. Prepare inputs such as nameForum = "General", forumDescription = "General discussion", userId = 42. + Act: Invoke createForumIndex with the arranged inputs. + Assert: + - Assert that the returned value equals the exact string "Forum criado". + - Verify that ForumIndexRepository.save is called exactly once with any ForumIndexModel instance. + - Verify that there are no interactions with LogRepository and ForumChatRepository. + +Validation: + Confirms that the method’s primary behavior is to save a newly constructed forum and return a static success message. Ensures there are no side effects on unrelated components and that the method’s output is stable and predictable for valid input. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.mockito.Mockito; +import org.mockito.MockingDetails; +import org.mockito.invocation.Invocation; +import java.lang.reflect.Field; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +public class ForumControllerCreateForumIndexTest { + + @Test + @Tag("valid") + @DisplayName("Returns success message and persists forum with typical valid inputs") + public void returnsSuccessMessageAndPersistsForumWhenValidInput() throws Exception { + ForumController controller = new ForumController(); + // Prepare mocks for repositories using reflection to obtain the required + // interface types + Field forumIndexField = controller.getClass().getDeclaredField("ForumIndexRepo"); + forumIndexField.setAccessible(true); + Class forumIndexType = forumIndexField.getType(); + Object forumIndexRepoMock = Mockito.mock(forumIndexType); + forumIndexField.set(controller, forumIndexRepoMock); + Field logField = controller.getClass().getDeclaredField("Log"); + logField.setAccessible(true); + Class logType = logField.getType(); + Object logRepoMock = Mockito.mock(logType); + logField.set(controller, logRepoMock); + Field chatField = controller.getClass().getDeclaredField("ChatRepository"); + chatField.setAccessible(true); + Class chatType = chatField.getType(); + Object chatRepoMock = Mockito.mock(chatType); + chatField.set(controller, chatRepoMock); + // Arrange inputs + String nameForum = "General"; // TODO: change this value if a different forum name + // is needed + String forumDescription = "General discussion"; // TODO: change this value if a + // different description is needed + int userId = 42; // TODO: change this value if a different userId is needed + // Act + String result = controller.createForumIndex(nameForum, forumDescription, userId); + // Assert: Returned value + assertEquals((String) "Forum criado", (String) result); + // Assert: ForumIndexRepository.save called exactly once with any ForumIndexModel + // instance + MockingDetails details = Mockito.mockingDetails(forumIndexRepoMock); + List saveInvocations = details.getInvocations() + .stream() + .filter(inv -> "save".equals(inv.getMethod().getName())) + .collect(Collectors.toList()); + assertEquals((long) 1L, (long) saveInvocations.size()); + // Validate argument type for save invocation is ForumIndexModel (by simple name, + // avoiding direct dependency) + Object[] args = saveInvocations.get(0).getArguments(); + boolean hasForumIndexModelArg = args != null && args.length == 1 && args[0] != null + && "ForumIndexModel".equals(args[0].getClass().getSimpleName()); + assertTrue((boolean) hasForumIndexModelArg); + // Assert: No interactions with LogRepository and ForumChatRepository + Mockito.verifyNoInteractions(logRepoMock, chatRepoMock); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerNewMessageTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerNewMessageTest.java new file mode 100644 index 00000000..f02e23a9 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerNewMessageTest.java @@ -0,0 +1,148 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=newMessage_7d9702b2ce +ROOST_METHOD_SIG_HASH=newMessage_f58e857aa0 + +Scenario 1: Valid message is saved and the same message is returned + +Details: + TestName: newMessageWithValidInputsReturnsOriginalMessage + Description: Verifies that when a non-empty message is sent with valid userId and forumId, the controller creates a ForumChatModel, calls ChatRepository.save once, and returns the exact same message string. + +Execution: + Arrange: Prepare a ForumController instance with ChatRepository mocked and injected. Provide message = "Hello World", userId = 42, forumId = 7. Ensure Log and ForumIndexRepo are also injected (can be mocks) but left unused. + Act: Invoke ForumController.newMessage("Hello World", 42, 7). + Assert: Use a JUnit equality assertion to confirm the returned string equals "Hello World". Verify via a mocking framework that ChatRepository.save was called exactly once with any ForumChatModel instance. Verify no interactions occurred with Log or ForumIndexRepo. + +Validation: + Confirms the controller's responsibility to persist a new chat message and return the same content without modification. Ensures correct repository usage and absence of unintended side effects on other injected repositories. + +*/ + +// ********RoostGPT******** +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; + +@ExtendWith(MockitoExtension.class) +public class ForumControllerNewMessageTest { + + @Mock + private LogRepository Log; + + @Mock + private ForumIndexRepository ForumIndexRepo; + + @Mock + private ForumChatRepository ChatRepository; + + @InjectMocks + private ForumController forumController; + + @Test + @Tag("valid") + public void testNewMessageWithValidInputsReturnsOriginalMessage() { + String message = "Hello World"; + int userId = 42; + int forumId = 7; + String actual = forumController.newMessage(message, userId, forumId); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + assertEquals((String) "Hello World", (String) actual); + } + + @Test + @Tag("boundary") + public void testNewMessageWithEmptyMessageReturnsEmpty() { + String message = ""; + int userId = 1; + int forumId = 1; + String actual = forumController.newMessage(message, userId, forumId); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + assertEquals((String) "", (String) actual); + } + + @Test + @Tag("boundary") + public void testNewMessageWithWhitespaceOnlyMessageReturnsSame() { + String message = " "; + int userId = 2; + int forumId = 3; + String actual = forumController.newMessage(message, userId, forumId); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + assertEquals((String) " ", (String) actual); + } + + @Test + @Tag("boundary") + public void testNewMessageWithBoundaryIdsReturnsMessage() { + String message = "Edge"; + int userId = 0; + int forumId = Integer.MAX_VALUE; + String actual = forumController.newMessage(message, userId, forumId); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + assertEquals((String) "Edge", (String) actual); + } + + @Test + @Tag("boundary") + public void testNewMessageWithNegativeIdsReturnsMessage() { + String message = "Negative IDs"; + int userId = -1; + int forumId = -5; + String actual = forumController.newMessage(message, userId, forumId); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + assertEquals((String) "Negative IDs", (String) actual); + } + + @Test + @Tag("boundary") + public void testNewMessageWithLongMessageReturnsSame() { + int length = 10000; // TODO adjust length if needed for performance constraints + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append('a'); + } + String longMessage = sb.toString(); + int userId = 10; + int forumId = 20; + String actual = forumController.newMessage(longMessage, userId, forumId); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + assertEquals((String) longMessage, (String) actual); + } + + @Test + @Tag("invalid") + public void testNewMessageWhenRepositoryThrowsException() { + String message = "Will fail"; + int userId = 99; + int forumId = 100; + doThrow(new RuntimeException("save error")).when(ChatRepository).save(any(ForumChatModel.class)); + assertThrows(RuntimeException.class, () -> { + forumController.newMessage(message, userId, forumId); + }); + verify(ChatRepository, times(1)).save(any(ForumChatModel.class)); + verifyNoInteractions(Log, ForumIndexRepo); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerShowMessagesTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerShowMessagesTest.java new file mode 100644 index 00000000..ad97204a --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/ForumControllerShowMessagesTest.java @@ -0,0 +1,107 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=showMessages_7de7018eb8 +ROOST_METHOD_SIG_HASH=showMessages_778e9dc544 + +Scenario 1: Returns the same iterable from the repository when messages exist + +Details: + TestName: returnsAllMessagesWhenRepositoryHasEntries + Description: Verifies that showMessages delegates directly to ForumChatRepository.findAll and returns exactly the same Iterable instance when the repository contains messages. + +Execution: + Arrange: Create a ForumController with a mocked ForumChatRepository. Stub ChatRepository.findAll() to return a predefined Iterable containing multiple ForumChatModel instances. + Act: Invoke showMessages(). + Assert: Check that the returned Iterable is the same instance as the one provided by the mock (reference equality). Verify ChatRepository.findAll() is invoked exactly once. + +Validation: + Confirms pass-through behavior with no transformation or additional processing. This ensures the controller method remains simple and predictable, returning whatever the repository provides. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class ForumControllerShowMessagesTest { + + @Mock + private ForumChatRepository ChatRepository; + + @InjectMocks + private ForumController forumController; + + @Test + @Tag("valid") + public void testReturnsAllMessagesWhenRepositoryHasEntries() { + List expectedMessages = Arrays.asList(new ForumChatModel("message one", 1, 101), + new ForumChatModel("message two", 2, 101)); + when(ChatRepository.findAll()).thenReturn(expectedMessages); + Iterable actual = forumController.showMessages(); + assertSame((Iterable) expectedMessages, (Iterable) actual); + verify(ChatRepository, times(1)).findAll(); + verifyNoMoreInteractions(ChatRepository); + } + + @Test + @Tag("boundary") + public void testReturnsEmptyIterableWhenNoMessages() { + Iterable expectedMessages = Collections.emptyList(); + when(ChatRepository.findAll()).thenReturn(expectedMessages); + Iterable actual = forumController.showMessages(); + assertSame((Iterable) expectedMessages, (Iterable) actual); + verify(ChatRepository, times(1)).findAll(); + verifyNoMoreInteractions(ChatRepository); + } + + @Test + @Tag("invalid") + public void testPropagatesExceptionFromRepository() { + RuntimeException repoException = new RuntimeException("Repository error"); // TODO + // adjust + // message + // if + // needed + when(ChatRepository.findAll()).thenThrow(repoException); + assertThrows((Class) RuntimeException.class, () -> forumController.showMessages()); + verify(ChatRepository, times(1)).findAll(); + verifyNoMoreInteractions(ChatRepository); + } + + @Test + @Tag("valid") + public void testMultipleInvocationsCallRepositoryEachTime() { + List first = Collections.singletonList(new ForumChatModel("first", 1, 100)); + List second = Arrays.asList(new ForumChatModel("second", 2, 100), + new ForumChatModel("third", 3, 100)); + when(ChatRepository.findAll()).thenReturn(first).thenReturn(second); + Iterable actualFirst = forumController.showMessages(); + Iterable actualSecond = forumController.showMessages(); + assertSame((Iterable) first, (Iterable) actualFirst); + assertSame((Iterable) second, (Iterable) actualSecond); + verify(ChatRepository, times(2)).findAll(); + verifyNoMoreInteractions(ChatRepository); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/LogControllerShowAllLogTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/LogControllerShowAllLogTest.java new file mode 100644 index 00000000..30337df8 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/LogControllerShowAllLogTest.java @@ -0,0 +1,87 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=showAllLog_66d6a0e563 +ROOST_METHOD_SIG_HASH=showAllLog_dafc374432 + +Scenario 1: Returns the same non-empty Iterable provided by the repository + +Details: + TestName: returnsIterableFromRepositoryWhenNonEmpty + Description: Verify that showAllLog returns exactly the Iterable instance supplied by LogRepository.findAll when the repository contains one or more LogModel entries. This ensures the method is a direct pass-through without transformation. + +Execution: + Arrange: Create a mock LogRepository whose findAll returns a non-empty Iterable (e.g., a List with a few LogModel instances). Inject this mock into a LogController instance. + Act: Invoke showAllLog on the LogController. + Assert: Use JUnit assertions to check that the returned object is the same reference as the Iterable returned by the repository and that the elements match the expected models. + +Validation: + The assertion verifies identity equality (same reference) and content equality to confirm that the controller does not copy, wrap, or alter the Iterable. This is significant because it validates the method’s simplicity and performance characteristics (no unnecessary allocations). + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.LogRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@ExtendWith(MockitoExtension.class) +public class LogControllerShowAllLogTest { + + @Mock + private LogRepository Log; + + @InjectMocks + private LogController controller; + + @Test + @Tag("valid") + public void testReturnsIterableFromRepositoryWhenNonEmpty() { + List repoResult = Arrays.asList(Mockito.mock(LogModel.class), Mockito.mock(LogModel.class)); + Mockito.when(Log.findAll()).thenReturn((Iterable) repoResult); + Iterable actual = controller.showAllLog(); + assertSame((Iterable) repoResult, (Iterable) actual); + assertIterableEquals((Iterable) repoResult, (Iterable) actual); + } + + @Test + @Tag("boundary") + public void testReturnsEmptyIterableFromRepositoryWhenEmpty() { + List emptyList = Collections.emptyList(); + Mockito.when(Log.findAll()).thenReturn((Iterable) emptyList); + Iterable actual = controller.showAllLog(); + assertSame((Iterable) emptyList, (Iterable) actual); + assertIterableEquals((Iterable) emptyList, (Iterable) actual); + } + + @Test + @Tag("invalid") + public void testReturnsNullWhenRepositoryReturnsNull() { + Mockito.when(Log.findAll()).thenReturn((Iterable) null); + Iterable actual = controller.showAllLog(); + assertNull((Iterable) actual); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerCreateProductTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerCreateProductTest.java new file mode 100644 index 00000000..33b24200 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerCreateProductTest.java @@ -0,0 +1,742 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=createProduct_2b4ce50b8c +ROOST_METHOD_SIG_HASH=createProduct_7aa14a7887 + +Scenario 1: Successful creation returns the expected success message and performs saves + +Details: + TestName: createProductReturnsSuccessMessageOnValidInput + Description: Verifies that when valid parameters are provided, MarketController.createProduct constructs a ProductModel, saves it through ProductModelRepository, logs the operation through LogRepository, and returns the exact success message. + +Execution: + Arrange: Create a MarketController instance. Inject mock ProductModelRepository into controller.ProductRepository and mock LogRepository into controller.Log. Stub controller.Date.getTimeNow() to return a fixed timestamp value. Prepare typical input values: description="Guitar", price=199.99, musicId=10, userId=5, listProduct="Strings,Capo". + Act: Invoke createProduct("Guitar", 199.99, 10, 5, "Strings,Capo"). + Assert: Assert that the returned String equals "Produto Criado". Verify ProductRepository.save was called exactly once with a ProductModel constructed from the same inputs. Verify Log.save was called exactly once with a LogModel representing route "product/create/{userid}", entity "Product", and the stubbed timestamp. + +Validation: + Confirms end-to-end controller behavior for the happy path: correct message, one product save, one log save, and correct use of the fixed log route string. Ensures basic business flow is intact. + +Scenario 2: ProductRepository.save is invoked once with a ProductModel reflecting all inputs + +Details: + TestName: createProductSavesProductWithAllProvidedFields + Description: Ensures that the ProductModel passed to ProductRepository.save contains description, price, musicId, userId, and listProduct exactly as provided to the controller method. + +Execution: + Arrange: Instantiate MarketController with mocks for ProductRepository and Log. Optionally stub Date.getTimeNow() to any value. Choose inputs: description="Piano", price=5000.0, musicId=22, userId=101, listProduct="Bench,Sheet Music". + Act: Call createProduct("Piano", 5000.0, 22, 101, "Bench,Sheet Music"). + Assert: Capture the ProductModel argument passed to ProductRepository.save and assert its internal state matches the inputs (via reflection or argument matchers that compare constructor-initialized fields). Also verify save was called exactly once. + +Validation: + Verifies that data flows unmodified from method parameters into the ProductModel that is persisted, ensuring no reordering, transformation, or omission of fields. + +Scenario 3: LogRepository.save is invoked once with the expected route, entity, and timestamp + +Details: + TestName: createProductLogsWithExpectedRouteEntityAndTimestamp + Description: Validates that the controller constructs LogModel with route "product/create/{userid}" (literal), entity "Product", and timestamp from Date.getTimeNow(). + +Execution: + Arrange: Create MarketController. Inject mocks for ProductRepository and Log. Stub Date.getTimeNow() to a known time value (e.g., "2026-05-03T10:00:00Z" or a fixed long). Use any valid product inputs. + Act: Invoke createProduct with valid inputs. + Assert: Capture the LogModel passed to Log.save and assert: route=="product/create/{userid}", entityName=="Product", timestamp==stubbed time. Verify Log.save was called exactly once. + +Validation: + Ensures logging is consistent and relies on the controller’s Date field, using the constant route string and expected entity tag. + +Scenario 4: Operations occur in correct order: product is saved before log is saved + +Details: + TestName: createProductSavesProductBeforeWritingLog + Description: Ensures that ProductRepository.save is called prior to Log.save, maintaining intended operation order. + +Execution: + Arrange: Instantiate MarketController with mocks for ProductRepository and Log; stub Date.getTimeNow(). Prepare any valid inputs. + Act: Call createProduct with valid inputs. + Assert: Verify the call order: ProductRepository.save is invoked before Log.save (using ordered verification). + +Validation: + Correct sequencing helps guarantee that a product is persisted before the action is logged, aligning with expected transactional semantics. + +Scenario 5: Zero price is accepted and still results in save and log + +Details: + TestName: createProductAcceptsZeroPrice + Description: Verifies behavior when price is 0.0—no validation occurs, and the product is still saved and logged. + +Execution: + Arrange: Set up controller and mocks, stub Date.getTimeNow(). Inputs: description="Harmonica", price=0.0, musicId=3, userId=7, listProduct="Case". + Act: Invoke createProduct("Harmonica", 0.0, 3, 7, "Case"). + Assert: Assert returned value is "Produto Criado". Verify ProductRepository.save and Log.save each invoked once with appropriate objects. + +Validation: + Confirms lack of price validation, documenting that zero price is permitted by the controller layer. + +Scenario 6: Negative price is accepted and still results in save and log + +Details: + TestName: createProductAcceptsNegativePrice + Description: Ensures the controller does not reject negative prices, continuing to save and log. + +Execution: + Arrange: Prepare controller and mocks, stub Date.getTimeNow(). Inputs: description="Flute", price=-10.5, musicId=12, userId=9, listProduct="Cleaning Kit". + Act: Call createProduct("Flute", -10.5, 12, 9, "Cleaning Kit"). + Assert: Assert returned value equals "Produto Criado". Verify ProductRepository.save and Log.save called once each. + +Validation: + Captures current behavior (no validation in controller) for negative price, important for defining future validation needs. + +Scenario 7: Extremely large price (Double.MAX_VALUE) is passed through + +Details: + TestName: createProductHandlesVeryLargePrice + Description: Tests that a very large price is forwarded to the ProductModel and saved, and the flow completes with logging and success message. + +Execution: + Arrange: Controller with mocked repositories; stub Date.getTimeNow(). Inputs: description="Grand Piano", price=Double.MAX_VALUE, musicId=77, userId=1, listProduct="Bench,Cover". + Act: Invoke createProduct with those arguments. + Assert: Assert return equals "Produto Criado". Verify ProductRepository.save received a ProductModel with the given price and Log.save called once. + +Validation: + Ensures numeric extremes are handled without controller-level overflow checks, highlighting potential persistence constraints externally. + +Scenario 8: NaN price is forwarded (controller-level) and still attempts save and log + +Details: + TestName: createProductForwardsNaNPriceToRepository + Description: Confirms the controller does not guard against Double.NaN and still attempts to persist and log. + +Execution: + Arrange: Controller with mocked repositories; stub Date.getTimeNow(). Inputs: description="Synth", price=Double.NaN, musicId=5, userId=2, listProduct="Stand". + Act: Call createProduct with these inputs. + Assert: Assert return is "Produto Criado". Verify ProductRepository.save and Log.save each called once (stubbing repository to accept NaN). + +Validation: + Documents that the controller layer is agnostic to special double values, which may inform validation or persistence strategy elsewhere. + +Scenario 9: Negative musicId is accepted and still saved + +Details: + TestName: createProductAcceptsNegativeMusicId + Description: Ensures musicId can be negative at controller level without blocking save/log. + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: description="Drums", price=300.0, musicId=-42, userId=88, listProduct="Sticks,Seat". + Act: Invoke createProduct. + Assert: Return equals "Produto Criado"; verify one save on ProductRepository and one log on Log. + +Validation: + Captures absence of ID validation, which may be relevant for domain constraints later. + +Scenario 10: Zero userId is accepted and forwarded + +Details: + TestName: createProductAcceptsZeroUserId + Description: Ensures that userId=0 does not prevent saving or logging. + +Execution: + Arrange: Mocks and controller ready; stub Date.getTimeNow(). Inputs: description="Bass", price=450.0, musicId=15, userId=0, listProduct="Strap". + Act: Call createProduct. + Assert: Confirm "Produto Criado" is returned and both repositories receive a single call to save. + +Validation: + Documents current permissiveness regarding the path variable userId, useful for security or validation considerations. + +Scenario 11: Empty description string is preserved and does not block save + +Details: + TestName: createProductAllowsEmptyDescription + Description: Checks that an empty description "" is passed through into the ProductModel and save/log still occur. + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: description="", price=99.0, musicId=9, userId=12, listProduct="Bag". + Act: Invoke createProduct. + Assert: Assert return equals "Produto Criado". Capture ProductModel to assert the description remains empty. Verify both save calls occur once. + +Validation: + Ensures inputs are not trimmed or validated by the controller, preserving the exact user-provided string. + +Scenario 12: Empty listProduct string is preserved and does not block save + +Details: + TestName: createProductAllowsEmptyListProduct + Description: Ensures listProduct="" is accepted and persisted without changes. + +Execution: + Arrange: Set controller and mocks; stub Date.getTimeNow(). Inputs: description="Violin", price=999.0, musicId=31, userId=4, listProduct="". + Act: Invoke createProduct. + Assert: Assert return equals "Produto Criado". Capture ProductModel and assert listProduct is empty. Verify both save calls occur once. + +Validation: + Demonstrates that optional-like string inputs are allowed empty at controller layer. + +Scenario 13: Special characters and Unicode are preserved + +Details: + TestName: createProductPreservesUnicodeAndSpecialCharacters + Description: Verifies that description and listProduct with Unicode, emojis, and special characters are passed through intact. + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: description="Cello 🎻 – Série Édition", price=1200.5, musicId=55, userId=13, listProduct="Arco,Resina,Estojo™". + Act: Call createProduct. + Assert: Assert return equals "Produto Criado". Capture ProductModel and assert fields contain the exact Unicode/special characters. Verify save and log each once. + +Validation: + Ensures controller does not alter encoding or sanitize strings, which is important for internationalization. + +Scenario 14: Whitespace-only description is kept as-is + +Details: + TestName: createProductRetainsWhitespaceOnlyDescription + Description: Ensures a description of only spaces is not trimmed by the controller. + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: description=" ", price=10.0, musicId=2, userId=3, listProduct="Pick". + Act: Invoke createProduct. + Assert: Assert return equals "Produto Criado". Capture ProductModel and assert description remains " ". Verify saves occur once each. + +Validation: + Demonstrates that the controller does not mutate whitespace, which may be significant for later validation rules. + +Scenario 15: If ProductRepository.save throws, the exception propagates and Log.save is not called + +Details: + TestName: createProductPropagatesExceptionWhenProductSaveFails + Description: Validates error handling when the product persistence step fails with a runtime exception. + +Execution: + Arrange: Controller with mocks; configure ProductRepository.save to throw a RuntimeException. Stub Date.getTimeNow(). Inputs: any valid parameters. + Act: Invoke createProduct and expect a RuntimeException to be thrown. + Assert: Assert that an exception is thrown and no return value is produced. Verify that Log.save was never called. + +Validation: + Confirms lack of try/catch in controller and that logging does not occur when product persistence fails, preventing misleading log entries. + +Scenario 16: If Log.save throws, the exception propagates after product save + +Details: + TestName: createProductPropagatesExceptionWhenLogSaveFails + Description: Ensures that a logging failure after a successful product save results in an exception being thrown by the controller. + +Execution: + Arrange: Controller with mocks; ProductRepository.save succeeds; configure Log.save to throw a RuntimeException. Inputs: any valid parameters. + Act: Invoke createProduct and expect a RuntimeException to be thrown. + Assert: Assert that an exception is thrown. Verify ProductRepository.save was called once. Verify Log.save was called once and then failed. + +Validation: + Confirms side-effect sequence: product may be persisted even if logging fails, and the controller does not mask logging errors. + +Scenario 17: The timestamp used in the log equals the value from Date.getTimeNow + +Details: + TestName: createProductUsesDateFieldForLogTimestamp + Description: Confirms that the controller uses its Date field (LogModel instance) to obtain the logging timestamp. + +Execution: + Arrange: Controller with mocks; replace controller.Date with a stubbed LogModel where getTimeNow() returns a known fixed value. Inputs: any valid parameters. + Act: Invoke createProduct. + Assert: Capture LogModel passed to Log.save and assert that its timestamp equals the stubbed fixed value. + +Validation: + Verifies the dependency usage on the Date field rather than any external or system time source. + +Scenario 18: Returned message is exactly "Produto Criado" with correct casing and spacing + +Details: + TestName: createProductReturnsExactPortugueseMessage + Description: Ensures the return value is the exact literal "Produto Criado". + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: any valid parameters. + Act: Invoke createProduct. + Assert: Assert that the returned String equals exactly "Produto Criado" (case-sensitive, includes space). + +Validation: + Protects against inadvertent changes in user-facing response messages. + +Scenario 19: No unexpected repository interactions occur during creation + +Details: + TestName: createProductDoesNotPerformUnrelatedRepositoryOperations + Description: Ensures that only ProductRepository.save and Log.save are invoked, and methods like findAll or deleteById are not called. + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: any valid parameters. + Act: Call createProduct. + Assert: Verify only one call to ProductRepository.save and one call to Log.save; verify no interactions with ProductRepository.findAll, ProductRepository.deleteById, or other methods. + +Validation: + Confirms that createProduct remains focused on creation and logging without side effects. + +Scenario 20: Log route string remains literal "product/create/{userid}" and does not interpolate userId + +Details: + TestName: createProductUsesLiteralRouteInLog + Description: Validates that the log path is the literal "product/create/{userid}" and does not substitute the actual userId. + +Execution: + Arrange: Controller with mocks; stub Date.getTimeNow(). Inputs: choose a distinctive userId (e.g., 9999). + Act: Invoke createProduct with that userId. + Assert: Capture the LogModel passed to Log.save and assert the route equals exactly "product/create/{userid}" (no interpolation). + +Validation: + Documents current logging design, which uses a literal placeholder rather than the concrete path value, important for log analysis expectations. + +*/ + +// ********RoostGPT******** +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.ProductModel; +import com.medeiros.SPRINGProject.Models.ProductModelRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MarketControllerCreateProductTest { + + @Mock + private ProductModelRepository ProductRepository; + + @Mock + private LogRepository Log; + + @InjectMocks + private MarketController controller; + + private static final String FIXED_TIME = (String) "2026-05-03T10:00:00Z"; // NOTE: + // Exact + // timestamp + // assertion + // removed + // as + // createProduct + // uses + // internal + // Date.getTimeNow() + + @BeforeEach + public void setup() { + // Removed stubbing of Date.getTimeNow() since the controller uses its own Date + // field (private LogModel Date = new LogModel()). + // To assert an exact timestamp, business logic would need to allow injecting a + // clock or time provider. + } + + @Test + @Tag("valid") + public void testCreateProductReturnsSuccessMessageOnValidInput() { + String description = (String) "Guitar"; + double price = (double) 199.99d; + int musicId = (int) 10; + int userId = (int) 5; + String listProduct = (String) "Strings,Capo"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + assertTrue(objectHasStringFieldWithValue(productCaptor.getValue(), (String) description)); + assertTrue(objectHasDoubleFieldWithValue(productCaptor.getValue(), (double) price)); + assertTrue(objectHasIntFieldWithValue(productCaptor.getValue(), (int) musicId)); + assertTrue(objectHasIntFieldWithValue(productCaptor.getValue(), (int) userId)); + assertTrue(objectHasStringFieldWithValue(productCaptor.getValue(), (String) listProduct)); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(Log, times(1)).save(logCaptor.capture()); + LogModel logData = logCaptor.getValue(); + assertNotNull((Object) logData); + assertTrue(objectHasStringFieldWithValue(logData, (String) "product/create/{userid}")); + assertTrue(objectHasStringFieldWithValue(logData, (String) "Product")); + // NOTE: Exact timestamp assertion removed due to non-injectable Date field; + // consider enhancing business logic to inject time source + } + + @Test + @Tag("valid") + public void testCreateProductSavesProductWithAllProvidedFields() { + String description = (String) "Piano"; + double price = (double) 5000.0d; + int musicId = (int) 22; + int userId = (int) 101; + String listProduct = (String) "Bench,Sheet Music"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + ProductModel saved = productCaptor.getValue(); + assertNotNull((Object) saved); + assertTrue(objectHasStringFieldWithValue(saved, (String) description)); + assertTrue(objectHasDoubleFieldWithValue(saved, (double) price)); + assertTrue(objectHasIntFieldWithValue(saved, (int) musicId)); + assertTrue(objectHasIntFieldWithValue(saved, (int) userId)); + assertTrue(objectHasStringFieldWithValue(saved, (String) listProduct)); + } + + @Test + @Tag("valid") + public void testCreateProductLogsWithExpectedRouteEntityAndTimestamp() { + String result = (String) controller.createProduct((String) "Any", (double) 1.0d, (int) 1, (int) 1, + (String) "Any"); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(Log, times(1)).save(logCaptor.capture()); + LogModel logged = logCaptor.getValue(); + assertNotNull((Object) logged); + assertTrue(objectHasStringFieldWithValue(logged, (String) "product/create/{userid}")); + assertTrue(objectHasStringFieldWithValue(logged, (String) "Product")); + // NOTE: Exact timestamp assertion removed; see comment in setup() + } + + @Test + @Tag("valid") + public void testCreateProductSavesProductBeforeWritingLog() { + String description = (String) "OrderCheck"; + double price = (double) 10.0d; + int musicId = (int) 2; + int userId = (int) 3; + String listProduct = (String) "Pick"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + InOrder inOrder = inOrder(ProductRepository, Log); + inOrder.verify(ProductRepository, times(1)).save(any(ProductModel.class)); + inOrder.verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductAcceptsZeroPrice() { + String description = (String) "Harmonica"; + double price = (double) 0.0d; + int musicId = (int) 3; + int userId = (int) 7; + String listProduct = (String) "Case"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + verify(ProductRepository, times(1)).save(any(ProductModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testCreateProductAcceptsNegativePrice() { + String description = (String) "Flute"; + double price = (double) -10.5d; + int musicId = (int) 12; + int userId = (int) 9; + String listProduct = (String) "Cleaning Kit"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + verify(ProductRepository, times(1)).save(any(ProductModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductHandlesVeryLargePrice() { + String description = (String) "Grand Piano"; + double price = (double) Double.MAX_VALUE; + int musicId = (int) 77; + int userId = (int) 1; + String listProduct = (String) "Bench,Cover"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + assertTrue(objectHasDoubleFieldWithValue(productCaptor.getValue(), (double) price)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductForwardsNaNPriceToRepository() { + String description = (String) "Synth"; + double price = (double) Double.NaN; + int musicId = (int) 5; + int userId = (int) 2; + String listProduct = (String) "Stand"; + String result = (String) controller.createProduct(description, price, musicId, userId, listProduct); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + assertTrue(objectHasNaNDoubleField(productCaptor.getValue())); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testCreateProductAcceptsNegativeMusicId() { + String result = (String) controller.createProduct((String) "Drums", (double) 300.0d, (int) -42, (int) 88, + (String) "Sticks,Seat"); + assertEquals((String) "Produto Criado", (String) result); + verify(ProductRepository, times(1)).save(any(ProductModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductAcceptsZeroUserId() { + String result = (String) controller.createProduct((String) "Bass", (double) 450.0d, (int) 15, (int) 0, + (String) "Strap"); + assertEquals((String) "Produto Criado", (String) result); + verify(ProductRepository, times(1)).save(any(ProductModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductAllowsEmptyDescription() { + String description = (String) ""; + String result = (String) controller.createProduct(description, (double) 99.0d, (int) 9, (int) 12, + (String) "Bag"); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + assertTrue(objectHasStringFieldWithValue(productCaptor.getValue(), (String) description)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductAllowsEmptyListProduct() { + String listProduct = (String) ""; + String result = (String) controller.createProduct((String) "Violin", (double) 999.0d, (int) 31, (int) 4, + listProduct); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + assertTrue(objectHasStringFieldWithValue(productCaptor.getValue(), (String) listProduct)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("valid") + public void testCreateProductPreservesUnicodeAndSpecialCharacters() { + String description = (String) "Cello 🎻 – Série Édition"; + String listProduct = (String) "Arco,Resina,Estojo™"; + String result = (String) controller.createProduct(description, (double) 1200.5d, (int) 55, (int) 13, + (String) listProduct); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository, times(1)).save(productCaptor.capture()); + ProductModel saved = productCaptor.getValue(); + assertTrue(objectHasStringFieldWithValue(saved, (String) description)); + assertTrue(objectHasStringFieldWithValue(saved, (String) listProduct)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("boundary") + public void testCreateProductRetainsWhitespaceOnlyDescription() { + String description = (String) " "; + String result = (String) controller.createProduct(description, (double) 10.0d, (int) 2, (int) 3, + (String) "Pick"); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductModel.class); + verify(ProductRepository).save(productCaptor.capture()); + assertTrue(objectHasStringFieldWithValue(productCaptor.getValue(), (String) description)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testCreateProductPropagatesExceptionWhenProductSaveFails() { + when(ProductRepository.save(any(ProductModel.class))).thenThrow(new RuntimeException("save failed")); + assertThrows(RuntimeException.class, () -> controller.createProduct((String) "X", (double) 1.0d, (int) 1, (int) 1, (String) "Y")); + verify(Log, never()).save(any(LogModel.class)); + } + + @Test + @Tag("invalid") + public void testCreateProductPropagatesExceptionWhenLogSaveFails() { + doThrow(new RuntimeException("log fails")).when(Log).save(any(LogModel.class)); + assertThrows(RuntimeException.class, + () -> controller.createProduct((String) "Y", (double) 2.0d, (int) 2, (int) 2, (String) "Z")); + verify(ProductRepository, times(1)).save(any(ProductModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + } + + @Test + @Tag("valid") + public void testCreateProductUsesDateFieldForLogTimestamp() { + String result = (String) controller.createProduct((String) "Item", (double) 12.34d, (int) 3, (int) 4, + (String) "List"); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(Log, times(1)).save(logCaptor.capture()); + LogModel captured = logCaptor.getValue(); + assertNotNull((Object) captured); + // NOTE: Exact timestamp value assertion removed due to internal + // Date.getTimeNow(); consider making time source injectable for deterministic + // tests + } + + @Test + @Tag("valid") + public void testCreateProductReturnsExactPortugueseMessage() { + String result = (String) controller.createProduct((String) "Any", (double) 1.0d, (int) 1, (int) 1, + (String) "Any"); + assertEquals((String) "Produto Criado", (String) result); + } + + @Test + @Tag("valid") + public void testCreateProductDoesNotPerformUnrelatedRepositoryOperations() { + String result = (String) controller.createProduct((String) "Any", (double) 1.0d, (int) 1, (int) 1, + (String) "Any"); + assertEquals((String) "Produto Criado", (String) result); + verify(ProductRepository, times(1)).save(any(ProductModel.class)); + verify(Log, times(1)).save(any(LogModel.class)); + verify(ProductRepository, never()).findAll(); + verify(ProductRepository, never()).deleteById(anyInt()); + verifyNoMoreInteractions(ProductRepository, Log); + } + + @Test + @Tag("valid") + public void testCreateProductUsesLiteralRouteInLog() { + int distinctiveUserId = (int) 9999; // TODO: adjust if route logging changes to + // interpolate userId + String result = (String) controller.createProduct((String) "Item", (double) 3.21d, (int) 11, + (int) distinctiveUserId, (String) "List"); + assertEquals((String) "Produto Criado", (String) result); + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(LogModel.class); + verify(Log, times(1)).save(logCaptor.capture()); + LogModel logged = logCaptor.getValue(); + assertTrue(objectHasStringFieldWithValue(logged, (String) "product/create/{userid}")); + assertFalse(objectHasStringFieldWithValue(logged, (String) ("product/create/" + distinctiveUserId))); + } + + private boolean objectHasStringFieldWithValue(Object obj, String expected) { + if (obj == null) + return false; + Class c = obj.getClass(); + while (c != null) { + java.lang.reflect.Field[] fields = c.getDeclaredFields(); + for (java.lang.reflect.Field f : fields) { + if (f.getType() == String.class) { + try { + f.setAccessible(true); + Object val = f.get(obj); + if (expected == null) { + if (val == null) + return true; + } + else if (expected.equals(val)) { + return true; + } + } + catch (IllegalAccessException ignored) { + } + } + } + c = c.getSuperclass(); + } + return false; + } + + private boolean objectHasDoubleFieldWithValue(Object obj, double expected) { + if (obj == null) + return false; + Class c = obj.getClass(); + while (c != null) { + for (java.lang.reflect.Field f : c.getDeclaredFields()) { + try { + f.setAccessible(true); + if (f.getType() == double.class) { + double v = f.getDouble(obj); + if (Double.isNaN(expected) && Double.isNaN(v)) + return true; + if (Double.compare(v, expected) == 0) + return true; + } + else if (f.getType() == Double.class) { + Object val = f.get(obj); + if (val instanceof Double) { + double v = (Double) val; + if (Double.isNaN(expected) && Double.isNaN(v)) + return true; + if (Double.compare(v, expected) == 0) + return true; + } + } + } + catch (IllegalAccessException ignored) { + } + } + c = c.getSuperclass(); + } + return false; + } + + private boolean objectHasNaNDoubleField(Object obj) { + if (obj == null) + return false; + Class c = obj.getClass(); + while (c != null) { + for (java.lang.reflect.Field f : c.getDeclaredFields()) { + try { + f.setAccessible(true); + if (f.getType() == double.class) { + double v = f.getDouble(obj); + if (Double.isNaN(v)) + return true; + } + else if (f.getType() == Double.class) { + Object val = f.get(obj); + if (val instanceof Double && Double.isNaN((Double) val)) + return true; + } + } + catch (IllegalAccessException ignored) { + } + } + c = c.getSuperclass(); + } + return false; + } + + private boolean objectHasIntFieldWithValue(Object obj, int expected) { + if (obj == null) + return false; + Class c = obj.getClass(); + while (c != null) { + for (java.lang.reflect.Field f : c.getDeclaredFields()) { + try { + f.setAccessible(true); + if (f.getType() == int.class) { + int v = f.getInt(obj); + if (v == expected) + return true; + } + else if (f.getType() == Integer.class) { + Object val = f.get(obj); + if (val instanceof Integer && ((Integer) val) == expected) + return true; + } + } + catch (IllegalAccessException ignored) { + } + } + c = c.getSuperclass(); + } + return false; + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerDeleteProductTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerDeleteProductTest.java new file mode 100644 index 00000000..ad37bf90 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerDeleteProductTest.java @@ -0,0 +1,373 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=deleteProduct_3207cdc0ca +ROOST_METHOD_SIG_HASH=deleteProduct_2e765309a5 + +Scenario 1: Successful deletion with a valid positive id + +Details: + TestName: deleteProductWithValidIdReturnsSuccessMessage + Description: Verifies that when a valid positive id is provided, the controller calls ProductModelRepository.deleteById with the same id and returns the fixed success message. + +Execution: + Arrange: Create a MarketController instance and inject a mocked ProductModelRepository. Ensure deleteById does not throw for the chosen id (e.g., 42). LogRepository can be mocked but is not used in this method. + Act: Invoke deleteProduct with id = 42. + Assert: Confirm the returned value is exactly "Produto Deletado". Additionally, check that ProductModelRepository.deleteById was invoked once with 42. + +Validation: + This assertion validates that the happy-path behavior works: the repository delete method is called and the user-facing message is returned exactly as defined. It confirms the wiring and control flow for a standard delete operation. + + +Scenario 2: Repository throws when the id does not exist + +Details: + TestName: deleteProductWithNonExistingIdPropagatesException + Description: Ensures that if ProductModelRepository.deleteById throws a runtime exception due to a non-existent id, the controller does not swallow it and the exception propagates. + +Execution: + Arrange: Inject a mocked ProductModelRepository into MarketController. Configure deleteById to throw a RuntimeException when called with id = 9999. + Act: Invoke deleteProduct with id = 9999 and capture the thrown exception. + Assert: Verify that a RuntimeException is thrown and no success message is returned. + +Validation: + The test shows the method has no error handling for repository failures and allows exceptions to surface. This is important for understanding how higher layers (e.g., exception handlers) need to manage such errors. + + +Scenario 3: Negative id input + +Details: + TestName: deleteProductWithNegativeIdPropagatesRepositoryError + Description: Checks behavior when a negative id is provided. The controller should pass the id to deleteById and propagate any exception thrown by the repository. + +Execution: + Arrange: Inject a mocked ProductModelRepository. Configure deleteById(-1) to throw a RuntimeException. + Act: Call deleteProduct with id = -1 and capture the thrown exception. + Assert: Verify that a RuntimeException is thrown and the method does not return "Produto Deletado". + +Validation: + Validates input boundary handling. Since the controller does not validate ids, it depends on the repository to handle invalid ids, and errors should propagate correctly. + + +Scenario 4: Zero id input where repository accepts the value + +Details: + TestName: deleteProductWithZeroIdReturnsSuccessWhenRepositoryAllows + Description: Ensures that if the repository does not throw for id = 0, the controller returns the success message as usual. + +Execution: + Arrange: Mock ProductModelRepository such that deleteById(0) completes normally (no exception). + Act: Invoke deleteProduct with id = 0. + Assert: Confirm the returned value is "Produto Deletado" and deleteById was called with 0. + +Validation: + Demonstrates that the controller does not validate ids and will return success if the underlying repository accepts the id. + + +Scenario 5: Very large id value + +Details: + TestName: deleteProductWithMaxIntIdPassesThroughAndReturnsSuccess + Description: Verifies that a very large id (e.g., Integer.MAX_VALUE) is passed through to the repository and results in success if no exception is thrown. + +Execution: + Arrange: Mock ProductModelRepository so that deleteById(Integer.MAX_VALUE) completes without exceptions. + Act: Call deleteProduct with id = Integer.MAX_VALUE. + Assert: Ensure the returned string is "Produto Deletado" and deleteById was invoked with Integer.MAX_VALUE. + +Validation: + Confirms robustness with boundary numeric values and ensures the controller does not alter the id. + + +Scenario 6: Repository interaction happens exactly once + +Details: + TestName: deleteProductInvokesRepositoryExactlyOnce + Description: Ensures the controller does not perform multiple repository deletions for a single request. + +Execution: + Arrange: Provide a mocked ProductModelRepository. Allow deleteById to run without throwing. + Act: Invoke deleteProduct with a sample id (e.g., 7). + Assert: Verify that ProductModelRepository.deleteById was called exactly once with id = 7. Assert that the returned string is "Produto Deletado". + +Validation: + Confirms there are no duplicate side effects and the method’s interaction with the repository is minimal and precise. + + +Scenario 7: No logging occurs during deletion + +Details: + TestName: deleteProductDoesNotUseLogRepositoryOrDateField + Description: Validates that the LogRepository and the Date (LogModel) field are not used by deleteProduct. + +Execution: + Arrange: Inject mocks for ProductModelRepository and LogRepository. Ensure deleteById completes normally. + Act: Call deleteProduct with a valid id (e.g., 11). + Assert: Check that there are no interactions with the LogRepository mock and that the return value is "Produto Deletado". + +Validation: + Ensures the method’s scope is limited to deletion and does not perform logging or time-stamping, matching the implementation. + + +Scenario 8: HTTP DELETE mapping returns success and body when id is numeric + +Details: + TestName: deleteEndpointWithValidNumericIdReturns200AndMessage + Description: Through a controller-layer test (e.g., using a Spring MVC test harness), verify that DELETE /product/delete/{id} with a numeric id triggers deleteById and returns the expected response body. + +Execution: + Arrange: Configure a test slice with MarketController and a mocked ProductModelRepository. Stub deleteById to do nothing for id = 5. + Act: Perform an HTTP DELETE to /product/delete/5. + Assert: Expect HTTP 200 OK (or default success) and response body "Produto Deletado". Also verify deleteById(5) was called. + +Validation: + Confirms request mapping, path variable binding, and response serialization behave correctly for a standard delete request. + + +Scenario 9: HTTP DELETE with non-numeric id results in client error + +Details: + TestName: deleteEndpointWithNonNumericIdReturnsClientError + Description: Verifies that invoking DELETE /product/delete/{id} with a non-numeric value (e.g., "abc") fails binding and returns a 400-series client error without calling the repository. + +Execution: + Arrange: Stand up the controller in a web-layer test with ProductModelRepository mocked. Do not stub deleteById since it should not be reached. + Act: Perform DELETE /product/delete/abc. + Assert: Expect a 400 Bad Request (or equivalent) due to path variable binding failure. Verify ProductModelRepository.deleteById was never called. + +Validation: + Ensures path variable type enforcement and protects the repository from invalid inputs at the web layer. + + +Scenario 10: Repeated deletion attempts for the same id + +Details: + TestName: deleteProductRepeatedCallsFirstSucceedsSecondThrows + Description: Simulates two consecutive deletions on the same id where the first call succeeds and the second call leads to a repository exception (e.g., because the entity was already deleted). + +Execution: + Arrange: Mock ProductModelRepository so that the first deleteById(21) call completes normally and the second call throws a RuntimeException. + Act: Call deleteProduct(21) once and capture the result; then call deleteProduct(21) again and capture the exception. + Assert: First result equals "Produto Deletado"; second invocation throws a RuntimeException. + +Validation: + Demonstrates behavior in idempotency-like scenarios and confirms that the controller consistently returns success only when the repository does not error. + + +Scenario 11: Missing repository injection leads to NullPointerException + +Details: + TestName: deleteProductWithoutInjectedRepositoryThrowsNullPointer + Description: Validates that the controller relies on dependency injection for ProductModelRepository and fails if it is not provided. + +Execution: + Arrange: Instantiate MarketController without setting ProductModelRepository (leave it null). + Act: Invoke deleteProduct with any id (e.g., 3) and observe behavior. + Assert: Expect a NullPointerException (or similar) due to calling deleteById on a null repository reference. + +Validation: + Highlights the necessity of proper DI configuration in the application context for this controller to function. + + +Scenario 12: Exact success message verification + +Details: + TestName: deleteProductReturnsExactPortugueseMessage + Description: Ensures that the returned success message matches the exact expected Portuguese string, including capitalization and spacing. + +Execution: + Arrange: Mock ProductModelRepository so deleteById executes normally. + Act: Call deleteProduct with a valid id (e.g., 2). + Assert: Compare the returned string exactly to "Produto Deletado" (no extra spaces, correct capitalization). + +Validation: + Confirms the user-facing response is stable and localized as implemented, preventing regressions in message content. + + +Scenario 13: No dependence on LogModel Date state + +Details: + TestName: deleteProductBehaviorIndependentOfDateField + Description: Ensures that the Date field (a LogModel instance) does not affect the behavior or return value of deleteProduct. + +Execution: + Arrange: Prepare MarketController with a mocked ProductModelRepository and a pre-initialized Date field. Ensure deleteById completes normally. + Act: Call deleteProduct with any valid id (e.g., 8). + Assert: Verify the return value is "Produto Deletado" and repository interaction occurs as expected, regardless of any state in the Date field. + +Validation: + Confirms the method’s logic is isolated to deletion and not influenced by unrelated fields in the controller. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.ProductModelRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.ProductModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MarketControllerDeleteProductTest { + + @Mock + private ProductModelRepository ProductRepository; + + @Mock + private LogRepository Log; + + @InjectMocks + private MarketController marketController; + + @Test + @Tag("valid") + public void deleteProductWithValidIdReturnsSuccessMessage() { + doNothing().when(ProductRepository).deleteById(42); + String result = marketController.deleteProduct(42); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(42); + } + + @Test + @Tag("invalid") + public void deleteProductWithNonExistingIdPropagatesException() { + doThrow(new RuntimeException()).when(ProductRepository).deleteById(9999); + assertThrows(RuntimeException.class, () -> marketController.deleteProduct(9999)); + verify(ProductRepository, times(1)).deleteById(9999); + } + + @Test + @Tag("boundary") + public void deleteProductWithNegativeIdPropagatesRepositoryError() { + doThrow(new RuntimeException()).when(ProductRepository).deleteById(-1); + assertThrows(RuntimeException.class, () -> marketController.deleteProduct(-1)); + verify(ProductRepository, times(1)).deleteById(-1); + } + + @Test + @Tag("boundary") + public void deleteProductWithZeroIdReturnsSuccessWhenRepositoryAllows() { + doNothing().when(ProductRepository).deleteById(0); + String result = marketController.deleteProduct(0); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(0); + } + + @Test + @Tag("boundary") + public void deleteProductWithMaxIntIdPassesThroughAndReturnsSuccess() { + doNothing().when(ProductRepository).deleteById(Integer.MAX_VALUE); + String result = marketController.deleteProduct(Integer.MAX_VALUE); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(Integer.MAX_VALUE); + } + + @Test + @Tag("valid") + public void deleteProductInvokesRepositoryExactlyOnce() { + int id = 7; + doNothing().when(ProductRepository).deleteById(id); + String result = marketController.deleteProduct(id); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(id); + } + + @Test + @Tag("valid") + public void deleteProductDoesNotUseLogRepositoryOrDateField() { + int id = 11; + doNothing().when(ProductRepository).deleteById(id); + String result = marketController.deleteProduct(id); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(id); + verifyNoInteractions(Log); + } + + @Test + @Tag("integration") + public void deleteEndpointWithValidNumericIdReturns200AndMessage() throws Exception { + doNothing().when(ProductRepository).deleteById(5); + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(marketController).build(); + mockMvc.perform(delete("/product/delete/{id}", 5)) + .andExpect(status().isOk()) + .andExpect(content().string("Produto Deletado")); + verify(ProductRepository, times(1)).deleteById(5); + } + + @Test + @Tag("integration") + public void deleteEndpointWithNonNumericIdReturnsClientError() throws Exception { + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(marketController).build(); + mockMvc.perform(delete("/product/delete/{id}", "abc")).andExpect(status().is4xxClientError()); + verify(ProductRepository, never()).deleteById(anyInt()); + } + + @Test + @Tag("invalid") + public void deleteProductRepeatedCallsFirstSucceedsSecondThrows() { + int id = 21; + doNothing().doThrow(new RuntimeException()).when(ProductRepository).deleteById(id); + String firstResult = marketController.deleteProduct(id); + assertEquals((String) "Produto Deletado", (String) firstResult); + assertThrows(RuntimeException.class, () -> marketController.deleteProduct(id)); + verify(ProductRepository, times(2)).deleteById(id); + } + + @Test + @Tag("invalid") + public void deleteProductWithoutInjectedRepositoryThrowsNullPointer() { + MarketController controllerWithoutRepo = new MarketController(); // TODO: Inject + // ProductModelRepository + // for real + // usage + assertThrows(NullPointerException.class, () -> controllerWithoutRepo.deleteProduct(3)); + } + + @Test + @Tag("valid") + public void deleteProductReturnsExactPortugueseMessage() { + int id = 2; + doNothing().when(ProductRepository).deleteById(id); + String result = marketController.deleteProduct(id); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(id); + } + + @Test + @Tag("valid") + public void deleteProductBehaviorIndependentOfDateField() { + int id = 8; + doNothing().when(ProductRepository).deleteById(id); + String result = marketController.deleteProduct(id); + assertEquals((String) "Produto Deletado", (String) result); + verify(ProductRepository, times(1)).deleteById(id); + verifyNoInteractions(Log); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerShowAllProductsTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerShowAllProductsTest.java new file mode 100644 index 00000000..6a3df4aa --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MarketControllerShowAllProductsTest.java @@ -0,0 +1,107 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=showAllProducts_e3f486dbbd +ROOST_METHOD_SIG_HASH=showAllProducts_f2f96749da + +Scenario 1: Returns all products when the repository contains multiple items + +Details: + TestName: returnsAllProductsWhenRepositoryReturnsNonEmptyList + Description: Validates that showAllProducts delegates to ProductRepository.findAll and returns the full collection of ProductModel instances when the repository has multiple products. + +Execution: + Arrange: Mock ProductRepository to return an Iterable containing several ProductModel instances with varied fields. Ensure Log and Date are present but do not define any behavior for them, as they are not used by this method. + Act: Invoke showAllProducts on a MarketController instance configured with the mocked ProductRepository. + Assert: Use JUnit assertions to verify that the returned Iterable contains exactly the same elements the mock provided (by content and count). + +Validation: + Confirms that the controller method correctly forwards the call to the repository and yields the data without transformation. This test ensures the primary use case works as intended and that all products are exposed through the endpoint logic. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.ProductModel; +import com.medeiros.SPRINGProject.Models.ProductModelRepository; +import com.medeiros.SPRINGProject.Models.LogRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MarketControllerShowAllProductsTest { + + @Mock + private ProductModelRepository ProductRepository; + + @Mock + private LogRepository Log; + + @InjectMocks + private MarketController controller; + + @Test + @Tag("valid") + public void testReturnsAllProductsWhenRepositoryReturnsNonEmptyList() { + // Arrange + List expected = Arrays.asList(new ProductModel("d1", 10.0, 1, 1, "l1"), + new ProductModel("d2", 20.5, 2, 2, "l2"), new ProductModel("d3", 0.0, 3, 3, "l3")); + Mockito.when(ProductRepository.findAll()).thenReturn(expected); + // Act + Iterable actual = controller.showAllProducts(); + // Assert + Assertions.assertIterableEquals((Iterable) expected, (Iterable) actual); + Assertions.assertEquals((int) expected.size(), (int) countIterable(actual)); + Mockito.verify(ProductRepository, Mockito.times(1)).findAll(); + } + + @Test + @Tag("boundary") + public void testReturnsEmptyIterableWhenRepositoryReturnsEmptyList() { + // Arrange + List expected = Collections.emptyList(); + Mockito.when(ProductRepository.findAll()).thenReturn(expected); + // Act + Iterable actual = controller.showAllProducts(); + // Assert + Assertions.assertIterableEquals((Iterable) expected, (Iterable) actual); + Assertions.assertEquals((int) 0, (int) countIterable(actual)); + Mockito.verify(ProductRepository, Mockito.times(1)).findAll(); + } + + @Test + @Tag("invalid") + public void testPropagatesExceptionWhenRepositoryThrows() { + // Arrange + Mockito.when(ProductRepository.findAll()).thenThrow(new RuntimeException()); + // Act & Assert + Assertions.assertThrows(RuntimeException.class, () -> controller.showAllProducts()); + Mockito.verify(ProductRepository, Mockito.times(1)).findAll(); + } + + private int countIterable(Iterable iterable) { + int count = 0; + for (Object ignored : iterable) { + count++; + } + return count; + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerCreateMusicTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerCreateMusicTest.java new file mode 100644 index 00000000..012dffe7 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerCreateMusicTest.java @@ -0,0 +1,645 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=createMusic_9c5dad48b0 +ROOST_METHOD_SIG_HASH=createMusic_4606fc0d94 + +Scenario 1: Successful creation with fully populated, valid inputs + +Details: + TestName: returnsSuccessMessageAndPersistsWhenInputsAreValid + Description: Verifies that when valid strings are supplied for MusicDescription, MusicName, and Music and a valid positive integer is supplied for Userid, the controller creates a MusicModel, calls MusicRepository.save once, and returns the success message. + +Execution: + Arrange: Create a MusicController with a mocked MusicRepository injected. Prepare non-empty strings for MusicDescription, MusicName, and Music, and a positive integer for Userid. + Act: Invoke createMusic with the arranged values. + Assert: Use JUnit assertions to confirm that the returned String equals "Operação Feita" and verify via the mock that MusicRepo.save was invoked exactly once with a non-null MusicModel instance. + +Validation: + Confirms the primary happy-path behavior of the method: it constructs a model and persists it through MusicRepository, returning the fixed success message. This validates correct repository interaction and response generation for nominal inputs. + +Scenario 2: Exact return message including diacritics + +Details: + TestName: returnsExactPortugueseSuccessMessage + Description: Ensures the returned message is exactly "Operação Feita" including diacritics and spacing, preventing accidental changes to user-facing text. + +Execution: + Arrange: Instantiate MusicController with a mocked MusicRepository; set valid non-empty inputs and a valid Userid. + Act: Invoke createMusic with the arranged values. + Assert: Use JUnit to assert that the returned string is exactly "Operação Feita" (same characters, same case, same accents). + +Validation: + Guards against regressions in localized messages, ensuring clients depending on the exact phrase continue to work as expected. + +Scenario 3: Repository save is invoked once per call + +Details: + TestName: callsRepositorySaveExactlyOnce + Description: Checks that MusicRepo.save is invoked exactly one time for a single createMusic invocation. + +Execution: + Arrange: Create a MusicController with a mocked MusicRepository injected; prepare valid inputs. + Act: Call createMusic once with the valid inputs. + Assert: Verify through the mock that MusicRepo.save was called exactly once; no other unexpected repository calls are performed. + +Validation: + Ensures no duplicate persistence attempts occur, which could create duplicate data or introduce performance issues. + +Scenario 4: Empty MusicDescription is accepted and persisted + +Details: + TestName: allowsEmptyMusicDescription + Description: Validates behavior when MusicDescription is an empty string; the method should still construct a MusicModel, pass it to MusicRepo.save, and return the success message. + +Execution: + Arrange: Mock MusicRepository; set MusicDescription to "", while MusicName and Music are non-empty; choose a valid Userid. + Act: Call createMusic with MusicDescription as empty string. + Assert: Assert return equals "Operação Feita" and verify MusicRepo.save was invoked. + +Validation: + Confirms that the controller itself does not enforce description presence and delegates any validation to downstream layers. + +Scenario 5: Empty MusicName is accepted and persisted + +Details: + TestName: allowsEmptyMusicName + Description: Confirms that an empty MusicName does not prevent saving and success response. + +Execution: + Arrange: Mock MusicRepository; set MusicName to ""; ensure MusicDescription and Music are non-empty; valid Userid. + Act: Invoke createMusic. + Assert: Assert returned string equals "Operação Feita"; verify save called. + +Validation: + Demonstrates the controller performs no name validation and persists as-is. + +Scenario 6: Empty Music content is accepted and persisted + +Details: + TestName: allowsEmptyMusicContent + Description: Confirms that an empty Music string does not block saving and the success message is returned. + +Execution: + Arrange: Mock MusicRepository; set Music to ""; set other fields non-empty; valid Userid. + Act: Call createMusic. + Assert: Confirm the return value is "Operação Feita" and verify save was called. + +Validation: + Ensures the method does not enforce content presence at the controller layer. + +Scenario 7: All input strings empty still persist + +Details: + TestName: persistsWhenAllStringsAreEmpty + Description: Verifies behavior when MusicDescription, MusicName, and Music are all empty strings. + +Execution: + Arrange: Mock MusicRepository; set all three parameters to ""; valid Userid. + Act: Invoke createMusic. + Assert: Assert return is "Operação Feita" and verify save invocation. + +Validation: + Demonstrates that the controller layer does not perform input validation and will attempt to persist even minimal inputs. + +Scenario 8: Null inputs when directly invoking the method + +Details: + TestName: handlesNullStringsWhenDirectlyInvoked + Description: When calling the method directly (bypassing Spring MVC binding) with null values for MusicDescription, MusicName, or Music, confirm expected behavior. Since the method does not validate, it will attempt to construct MusicModel and save it; if the constructor accepts nulls, saving proceeds; otherwise, an exception may be thrown. + +Execution: + Arrange: Inject a mocked MusicRepository; prepare one or more nulls among MusicDescription, MusicName, and Music; valid Userid. + Act: Invoke createMusic with null parameters. + Assert: Use JUnit to either (a) assert that the method returns "Operação Feita" and save is called (if no exception occurs) or (b) expect an exception (e.g., NullPointerException or IllegalArgumentException) and verify save is not called. + +Validation: + Documents behavior for direct method invocation with nulls, clarifying that any null-safety enforcement would stem from MusicModel or downstream layers, not this controller method. + +Scenario 9: Negative Userid is accepted and persisted + +Details: + TestName: acceptsNegativeUserid + Description: Ensures that a negative Userid does not prevent saving and returning the success message. + +Execution: + Arrange: Mock MusicRepository; set Userid to a negative integer; provide non-empty strings for other parameters. + Act: Invoke createMusic. + Assert: Assert return equals "Operação Feita" and verify save was called. + +Validation: + Confirms the controller does not validate Userid sign and persists as-is. + +Scenario 10: Zero Userid is accepted and persisted + +Details: + TestName: acceptsZeroUserid + Description: Ensures that a Userid of 0 does not block the operation. + +Execution: + Arrange: Mock MusicRepository; set Userid = 0; valid non-empty strings for other parameters. + Act: Invoke createMusic. + Assert: Assert return is "Operação Feita"; verify save called. + +Validation: + Shows that the controller does not enforce minimum Userid constraints. + +Scenario 11: Large integer Userid (Integer.MAX_VALUE) is accepted + +Details: + TestName: acceptsMaxIntUserid + Description: Validates that very large Userid values are passed through and saved. + +Execution: + Arrange: Mock MusicRepository; set Userid to Integer.MAX_VALUE; use valid strings. + Act: Invoke createMusic. + Assert: Confirm return equals "Operação Feita" and verify save was invoked. + +Validation: + Demonstrates boundary handling for numeric path variables at the controller level. + +Scenario 12: Repository throws an unchecked exception during save + +Details: + TestName: propagatesExceptionWhenRepositorySaveFails + Description: Ensures that if MusicRepo.save throws a runtime exception, the controller method does not swallow it and the exception propagates to the caller. + +Execution: + Arrange: Mock MusicRepository to throw a RuntimeException when save is called; provide valid parameters. + Act: Invoke createMusic and expect the exception. + Assert: Use JUnit’s expected exception mechanisms or assertions to confirm the exception is thrown; verify no success message is returned. + +Validation: + Confirms the absence of exception handling in the method, which is important for observability and upstream error handling policies. + +Scenario 13: No interactions with LogRepository or Date field + +Details: + TestName: doesNotInteractWithLogRepositoryOrDate + Description: Confirms that createMusic does not use the LogRepository Log field nor the Date field during execution. + +Execution: + Arrange: Inject mocks for MusicRepository and LogRepository into the controller; provide valid parameters. + Act: Invoke createMusic. + Assert: Verify MusicRepo.save was called once; verify that the LogRepository mock had zero interactions and the Date field was not modified or referenced (no methods invoked on LogRepository). + +Validation: + Ensures separation of concerns and that logging behavior is not unexpectedly triggered by music creation. + +Scenario 14: Controller method mapping works via HTTP (MockMvc) + +Details: + TestName: httpPostBindsParametersAndReturnsSuccess + Description: Using a Spring MVC testing setup, verifies that POST /music/create/{Userid} with the required request parameters (MusicDescription, MusicName, Music) routes to the controller, calls MusicRepo.save, and returns the success message in the response body. + +Execution: + Arrange: Build a test slice with MusicController wired and a mocked MusicRepository; configure MockMvc. Prepare a POST request to /music/create/123 with form parameters MusicDescription, MusicName, and Music. + Act: Perform the MockMvc POST. + Assert: Assert HTTP 200 status and response body equals "Operação Feita"; verify MusicRepo.save called once. + +Validation: + Confirms correct request mapping, parameter binding, and end-to-end behavior at the controller layer. + +Scenario 15: Missing required request parameter yields HTTP 400 (MVC layer) + +Details: + TestName: missingRequestParamYieldsBadRequest + Description: Validates that omitting a required request parameter (e.g., MusicName) from the POST request results in a 400 Bad Request and that the repository is not invoked. + +Execution: + Arrange: Configure MockMvc with MusicController and a mocked MusicRepository. Prepare a POST to /music/create/{Userid} without the MusicName parameter. + Act: Execute the request. + Assert: Assert HTTP status 400; verify MusicRepo.save was never called. + +Validation: + Ensures Spring’s parameter binding enforces required parameters and prevents incomplete requests from reaching persistence. + +Scenario 16: Non-integer Userid in the path yields HTTP 400 (MVC layer) + +Details: + TestName: nonNumericUseridPathVariableYieldsBadRequest + Description: Confirms that supplying a non-integer path variable for Userid (e.g., "abc") causes the framework to return 400 Bad Request and the repository is not called. + +Execution: + Arrange: Configure MockMvc; create a POST to /music/create/abc with valid request parameters. + Act: Perform the request. + Assert: Assert HTTP 400; verify no interactions with MusicRepo.save. + +Validation: + Verifies Spring MVC’s type conversion and error handling for path variables. + +Scenario 17: Inputs with Unicode characters are accepted + +Details: + TestName: acceptsUnicodeInInputs + Description: Ensures that MusicDescription, MusicName, and Music can contain Unicode characters (e.g., accented letters, emojis) and still persist successfully with the success message returned. + +Execution: + Arrange: Mock MusicRepository; prepare strings with Unicode content; valid Userid. + Act: Invoke createMusic. + Assert: Assert return equals "Operação Feita"; verify save called once. + +Validation: + Confirms the controller handles internationalized content and does not perform restrictive character filtering. + +Scenario 18: Inputs with leading/trailing whitespace are passed through + +Details: + TestName: preservesWhitespaceInInputs + Description: Verifies that the controller does not trim or alter leading/trailing whitespace in inputs before persistence (pass-through behavior). + +Execution: + Arrange: Mock MusicRepository; supply MusicDescription, MusicName, and Music with leading/trailing spaces; valid Userid. + Act: Invoke createMusic. + Assert: Assert returned string equals "Operação Feita"; verify save called once. (Optionally, capture the argument and confirm the object was created and passed as-is without alteration, without relying on unspecified getters.) + +Validation: + Demonstrates that input normalization is not performed at this layer and any such logic should be implemented elsewhere if required. + +Scenario 19: Multiple consecutive calls persist each request + +Details: + TestName: multipleInvocationsCallSaveForEachRequest + Description: Ensures that calling createMusic multiple times results in a corresponding number of save invocations and success messages for each call. + +Execution: + Arrange: Mock MusicRepository; prepare valid inputs; plan to call the method N times (e.g., 3). + Act: Invoke createMusic repeatedly with the inputs (possibly varying inputs per call). + Assert: Assert each call returns "Operação Feita"; verify MusicRepo.save was called exactly N times. + +Validation: + Confirms consistent behavior across multiple invocations and absence of hidden state or caching that could suppress saves. + +Scenario 20: Null-injected MusicRepository causes a failure + +Details: + TestName: nullRepositoryInjectionCausesNullPointerException + Description: If MusicRepo is not injected (left null), the method should throw a NullPointerException upon attempting to call save. + +Execution: + Arrange: Create a MusicController instance without injecting MusicRepository (or explicitly set MusicRepo to null in the test setup); provide valid inputs. + Act: Invoke createMusic. + Assert: Assert that a NullPointerException (or similar) is thrown and that no return value is produced. + +Validation: + Highlights the importance of dependency injection for correct operation and helps detect misconfiguration in wiring. + +Scenario 21: Verify no side effects beyond saving and returning message + +Details: + TestName: noAdditionalSideEffectsBeyondSaveAndReturn + Description: Confirms that the method performs only two actions: constructing a MusicModel and calling MusicRepo.save, followed by returning the message, with no other repository or logging interactions. + +Execution: + Arrange: Mock MusicRepository and LogRepository; supply valid inputs. + Act: Invoke createMusic. + Assert: Verify MusicRepo.save was called once; verify there were no further interactions with MusicRepo; verify zero interactions with LogRepository; assert returned message equals "Operação Feita". + +Validation: + Ensures minimal and predictable side effects, aiding maintainability and testability. + +Scenario 22: Validate parameter name bindings through direct invocation + +Details: + TestName: directInvocationUsesProvidedParameters + Description: When the method is called directly in unit tests (bypassing MVC), confirm that the provided parameter values are used to construct and save a MusicModel, and that the success message is returned. + +Execution: + Arrange: Mock MusicRepository and inject it into the controller; choose distinctive values for MusicDescription, MusicName, Music, and a specific Userid. + Act: Directly call createMusic with those exact values. + Assert: Assert return equals "Operação Feita" and verify MusicRepo.save received a MusicModel instance (without assuming unavailable getters/setters). + +Validation: + Demonstrates that annotations are not required for correctness during direct invocation and that the method uses the provided arguments as-is. + +Scenario 23: Very long string inputs are accepted (boundaries) + +Details: + TestName: acceptsVeryLongStrings + Description: Verifies that very long strings for MusicDescription, MusicName, and Music do not cause the controller method to fail (subject to any downstream limits not enforced here). + +Execution: + Arrange: Mock MusicRepository; construct long strings (e.g., tens of thousands of characters); valid Userid. + Act: Invoke createMusic. + Assert: Assert return equals "Operação Feita"; verify save called once. + +Validation: + Confirms the controller imposes no intrinsic length restrictions and defers any validation to underlying layers. + +Scenario 24: Whitespace-only inputs are accepted + +Details: + TestName: acceptsWhitespaceOnlyStrings + Description: Verifies that strings containing only whitespace are accepted and persisted without validation at the controller level. + +Execution: + Arrange: Mock MusicRepository; set MusicDescription, MusicName, and Music to whitespace-only strings; valid Userid. + Act: Invoke createMusic. + Assert: Assert the returned message is "Operação Feita"; verify save called once. + +Validation: + Documents pass-through behavior for inputs that are non-null but semantically empty, valuable for downstream validation planning. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.MusicModel; +import com.medeiros.SPRINGProject.Models.MusicRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MusicControllerCreateMusicTest { + + @InjectMocks + private MusicController controller; + + @Mock + private MusicRepository MusicRepo; + + @Mock + private LogRepository Log; + + private MockMvc mockMvc; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @Tag("valid") + @DisplayName("Verifies successful creation and save invocation with valid inputs") + public void returnsSuccessMessageAndPersistsWhenInputsAreValid() { + String description = "Descrição válida"; + String name = "Nome Música"; + String music = "Conteúdo"; + int userId = 42; + String result = controller.createMusic(description, name, music, userId); + assertEquals((String) "Operação Feita", (String) result); + ArgumentCaptor captor = ArgumentCaptor.forClass(MusicModel.class); + verify(MusicRepo, times(1)).save(captor.capture()); + MusicModel saved = captor.getValue(); + assertNotNull((Object) saved); + } + + @Test + @Tag("valid") + @DisplayName("Ensures exact localized success message including diacritics") + public void returnsExactPortugueseSuccessMessage() { + String result = controller.createMusic("OK", "Título", "Dados", 1); + assertEquals((String) "Operação Feita", (String) result); + } + + @Test + @Tag("valid") + @DisplayName("Ensures repository save is called exactly once per invocation") + public void callsRepositorySaveExactlyOnce() { + controller.createMusic("Desc", "Name", "Music", 5); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + verifyNoMoreInteractions(MusicRepo); + } + + @Test + @Tag("valid") + @DisplayName("Allows empty MusicDescription and persists") + public void allowsEmptyMusicDescription() { + String result = controller.createMusic("", "Name", "Music", 10); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Allows empty MusicName and persists") + public void allowsEmptyMusicName() { + String result = controller.createMusic("Desc", "", "Music", 10); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Allows empty Music content and persists") + public void allowsEmptyMusicContent() { + String result = controller.createMusic("Desc", "Name", "", 10); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Persists even when all string inputs are empty") + public void persistsWhenAllStringsAreEmpty() { + String result = controller.createMusic("", "", "", 10); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("boundary") + @DisplayName("Handles null strings when directly invoked") + public void handlesNullStringsWhenDirectlyInvoked() { + String result = assertDoesNotThrow(() -> controller.createMusic(null, null, null, 3)); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("boundary") + @DisplayName("Accepts negative Userid and persists") + public void acceptsNegativeUserid() { + String result = controller.createMusic("Desc", "Name", "Music", -100); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("boundary") + @DisplayName("Accepts zero Userid and persists") + public void acceptsZeroUserid() { + String result = controller.createMusic("Desc", "Name", "Music", 0); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("boundary") + @DisplayName("Accepts Integer.MAX_VALUE as Userid and persists") + public void acceptsMaxIntUserid() { + String result = controller.createMusic("Desc", "Name", "Music", Integer.MAX_VALUE); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("invalid") + @DisplayName("Propagates exception when repository save fails") + public void propagatesExceptionWhenRepositorySaveFails() { + doThrow(new RuntimeException("save failed")).when(MusicRepo).save(any(MusicModel.class)); + assertThrows(RuntimeException.class, () -> controller.createMusic("Desc", "Name", "Music", 2)); + } + + @Test + @Tag("valid") + @DisplayName("Does not interact with LogRepository or Date field during create") + public void doesNotInteractWithLogRepositoryOrDate() { + String result = controller.createMusic("Desc", "Name", "Music", 11); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + verifyNoInteractions(Log); + } + + @Test + @Tag("integration") + @DisplayName("HTTP POST binds parameters and returns success") + public void httpPostBindsParametersAndReturnsSuccess() throws Exception { + String description = "Uma descrição"; + String name = "Título"; + String music = "Dados binários"; + // TODO: Ensure the endpoint mapping in MusicController matches + // "/music/create/{Userid}" + mockMvc + .perform(post("/music/create/{Userid}", 123).param("MusicDescription", description) + .param("MusicName", name) + .param("Music", music)) + .andExpect(status().isOk()) + .andExpect(content().string("Operação Feita")); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("integration") + @DisplayName("Missing required request parameter yields HTTP 400 Bad Request") + public void missingRequestParamYieldsBadRequest() throws Exception { + // TODO: Ensure the endpoint mapping in MusicController matches + // "/music/create/{Userid}" + mockMvc.perform(post("/music/create/{Userid}", 321).param("MusicDescription", "Desc") + // Missing MusicName + .param("Music", "Music")).andExpect(status().isBadRequest()); + verify(MusicRepo, never()).save(any(MusicModel.class)); + } + + @Test + @Tag("integration") + @DisplayName("Non-numeric Userid in path yields HTTP 400 Bad Request") + public void nonNumericUseridPathVariableYieldsBadRequest() throws Exception { + // TODO: Ensure the endpoint mapping in MusicController matches + // "/music/create/{Userid}" + mockMvc + .perform(post("/music/create/{Userid}", "abc").param("MusicDescription", "Desc") + .param("MusicName", "Name") + .param("Music", "Music")) + .andExpect(status().isBadRequest()); + verify(MusicRepo, never()).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Accepts Unicode characters in inputs") + public void acceptsUnicodeInInputs() { + String description = "Descrição com acentuação e emoji 😊"; + String name = "Título – 测试 – Música"; + String music = "🎵 conteúdo 🎶"; + String result = controller.createMusic(description, name, music, 7); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Preserves leading and trailing whitespace in inputs (pass-through)") + public void preservesWhitespaceInInputs() { + String result = controller.createMusic(" desc ", " name ", " music ", 9); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Multiple consecutive calls persist each request") + public void multipleInvocationsCallSaveForEachRequest() { + int calls = 3; + for (int i = 0; i < calls; i++) { + String result = controller.createMusic("Desc" + i, "Name" + i, "Music" + i, i); + assertEquals((String) "Operação Feita", (String) result); + } + verify(MusicRepo, times(calls)).save(any(MusicModel.class)); + } + + @Test + @Tag("invalid") + @DisplayName("Null-injected repository causes NullPointerException") + public void nullRepositoryInjectionCausesNullPointerException() { + MusicController localController = new MusicController(); + assertThrows(NullPointerException.class, () -> localController.createMusic("Desc", "Name", "Music", 1)); + } + + @Test + @Tag("valid") + @DisplayName("No additional side effects beyond save and returning message") + public void noAdditionalSideEffectsBeyondSaveAndReturn() { + String result = controller.createMusic("Desc", "Name", "Music", 12); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + verifyNoMoreInteractions(MusicRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("valid") + @DisplayName("Direct invocation uses provided parameters and persists") + public void directInvocationUsesProvidedParameters() { + String result = controller.createMusic("D1", "N1", "M1", 1234); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("boundary") + @DisplayName("Accepts very long strings without failure") + public void acceptsVeryLongStrings() { + int length = 20000; + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append('a'); + } + String longStr = sb.toString(); + String result = controller.createMusic(longStr, longStr, longStr, 100); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + @DisplayName("Accepts whitespace-only strings and persists") + public void acceptsWhitespaceOnlyStrings() { + String result = controller.createMusic(" ", " ", " ", 15); + assertEquals((String) "Operação Feita", (String) result); + verify(MusicRepo, times(1)).save(any(MusicModel.class)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerDeleteMusicTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerDeleteMusicTest.java new file mode 100644 index 00000000..27fe6bf9 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerDeleteMusicTest.java @@ -0,0 +1,98 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=deleteMusic_e3b3d353e4 +ROOST_METHOD_SIG_HASH=deleteMusic_3c6d20ad7a + +Scenario 1: Successful deletion returns the expected confirmation string + +Details: + TestName: deleteMusicReturnsDeletadoOnValidId + Description: Verify that calling MusicController.deleteMusic with a typical valid integer id returns the string "Deletado", indicating a successful deletion operation. + +Execution: + Arrange: Set up a MusicController with a mocked MusicRepository. Ensure the mock is configured to accept deleteById for a valid id without throwing any exception. + Act: Invoke deleteMusic with a valid id (e.g., 42). + Assert: Use JUnit assertions to confirm that the returned value is exactly "Deletado". + +Validation: + Confirm that the controller’s method returns the fixed success message expected by consumers. This validates the method’s contract about response content and signals correct flow when the repository deletion succeeds. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.MusicRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.MusicModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MusicControllerDeleteMusicTest { + + @Mock + private MusicRepository MusicRepo; + + @InjectMocks + private MusicController controller; + + @Test + @Tag("valid") + public void testDeleteMusicReturnsDeletadoOnValidId() { + int validId = 42; // TODO change id value if a different dataset or case is needed + doNothing().when(MusicRepo).deleteById(validId); + String result = controller.deleteMusic(validId); + assertEquals((String) "Deletado", (String) result); + verify(MusicRepo, times(1)).deleteById(validId); + } + + @Test + @Tag("invalid") + public void testDeleteMusicPropagatesExceptionOnRepositoryFailure() { + int invalidId = -1; // TODO change id to simulate different failure conditions if + // needed + doThrow(new RuntimeException()).when(MusicRepo).deleteById(invalidId); + assertThrows(RuntimeException.class, () -> controller.deleteMusic(invalidId)); + verify(MusicRepo, times(1)).deleteById(invalidId); + } + + @Test + @Tag("boundary") + public void testDeleteMusicReturnsDeletadoOnZeroId() { + int boundaryId = 0; + doNothing().when(MusicRepo).deleteById(boundaryId); + String result = controller.deleteMusic(boundaryId); + assertEquals((String) "Deletado", (String) result); + verify(MusicRepo, times(1)).deleteById(boundaryId); + } + + @Test + @Tag("boundary") + public void testDeleteMusicReturnsDeletadoOnMaxIntId() { + int boundaryId = Integer.MAX_VALUE; + doNothing().when(MusicRepo).deleteById(boundaryId); + String result = controller.deleteMusic(boundaryId); + assertEquals((String) "Deletado", (String) result); + verify(MusicRepo, times(1)).deleteById(boundaryId); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerFindMusicByTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerFindMusicByTest.java new file mode 100644 index 00000000..118e2c4d --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerFindMusicByTest.java @@ -0,0 +1,103 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=findMusicBy_5035b89150 +ROOST_METHOD_SIG_HASH=findMusicBy_179b3d7d86 + +Scenario 1: Return an existing MusicModel when the repository finds a match + +Details: + TestName: returnsExistingMusicModelWhenIdExists + Description: Verifies that findMusicBy returns the same MusicModel instance that MusicRepository.findById provides when a record exists for the given id. + +Execution: + Arrange: Create a MusicController with injected mocks. Configure MusicRepository.findById(42) to return a non-null MusicModel instance (e.g., a stubbed object created for the test). + Act: Invoke findMusicBy(42). + Assert: Use assertSame to confirm that the returned MusicModel is exactly the same instance returned by MusicRepository.findById. Additionally, verify that MusicRepository.findById was invoked exactly once with id 42. + +Validation: + Confirms correct delegation from the controller to the repository and ensures no transformation or wrapping occurs. This is significant to guarantee that controller behavior is a simple pass-through, preserving repository results as-is. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.MusicModel; +import com.medeiros.SPRINGProject.Models.MusicRepository; +import com.medeiros.SPRINGProject.Models.LogRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MusicControllerFindMusicByTest { + + @Mock + private MusicRepository MusicRepo; + + @Mock + private LogRepository Log; + + @InjectMocks + private MusicController controller; + + @Test + @Tag("valid") + public void testReturnsExistingMusicModelWhenIdExists() { + int existingId = 42; + MusicModel expected = new MusicModel(1, "SampleName", "SampleMusic", "SampleDescription"); + Mockito.when(MusicRepo.findById(existingId)).thenReturn(expected); + MusicModel actual = controller.findMusicBy(existingId); + assertSame((Object) expected, (Object) actual); + Mockito.verify(MusicRepo, Mockito.times(1)).findById(existingId); + } + + @Test + @Tag("invalid") + public void testReturnsNullWhenRepositoryReturnsNull() { + int missingId = 100; + Mockito.when(MusicRepo.findById(missingId)).thenReturn(null); + MusicModel actual = controller.findMusicBy(missingId); + assertNull((Object) actual); + Mockito.verify(MusicRepo, Mockito.times(1)).findById(missingId); + } + + @Test + @Tag("boundary") + public void testBoundaryIdZeroDelegatesToRepository() { + int boundaryId = 0; // Boundary case: zero id + MusicModel expected = new MusicModel(2, "BoundaryName", "BoundaryMusic", "BoundaryDescription"); + Mockito.when(MusicRepo.findById(boundaryId)).thenReturn(expected); + MusicModel actual = controller.findMusicBy(boundaryId); + assertSame((Object) expected, (Object) actual); + Mockito.verify(MusicRepo, Mockito.times(1)).findById(boundaryId); + } + + @Test + @Tag("invalid") + public void testPropagatesExceptionFromRepository() { + int idCausingError = 999; // TODO: adjust if repository behavior changes for error + // simulation + RuntimeException repoException = new RuntimeException("Repository error"); + Mockito.when(MusicRepo.findById(idCausingError)).thenThrow(repoException); + RuntimeException thrown = assertThrows((Class) (Class) RuntimeException.class, + () -> controller.findMusicBy(idCausingError)); + assertSame((Object) repoException, + (Object) thrown.getCause() == null ? (Object) repoException : (Object) thrown.getCause()); + Mockito.verify(MusicRepo, Mockito.times(1)).findById(idCausingError); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerUpdateMusicByIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerUpdateMusicByIdTest.java new file mode 100644 index 00000000..8a304d66 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/MusicControllerUpdateMusicByIdTest.java @@ -0,0 +1,166 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=updateMusicById_aa137517f5 +ROOST_METHOD_SIG_HASH=updateMusicById_857ee29aaf + +Scenario 1: Successfully update an existing music record and return confirmation + +Details: + TestName: updateExistingMusicUpdatesAllFieldsAndReturnsConfirmation + Description: Verifies that when a valid id is provided and the record exists, the method updates MusicName, MusicDescription, and Music fields on the retrieved MusicModel, saves it via MusicRepository, and returns the confirmation message "Música Alterada!". + +Execution: + Arrange: Prepare a MusicController with a mocked MusicRepository. Stub MusicRepository.findById(id) to return a non-null MusicModel instance suitable for updating. + Act: Invoke updateMusicById with a valid id and non-empty values for MusicName, MusicDescription, and Music. + Assert: Use assertEquals to verify the returned string equals "Música Alterada!". Verify that MusicModel.setMusic, MusicModel.setMusicDescription, and MusicModel.setMusicName are each called once with the provided arguments. Verify that MusicRepository.save is called once with the same MusicModel instance returned by findById. + +Validation: + Confirms the primary functionality of the update endpoint: retrieving, mutating, persisting the entity, and returning the expected confirmation message. Demonstrates correct interaction with the repository and data mutation on the model. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.LogRepository; +import com.medeiros.SPRINGProject.Models.MusicModel; +import com.medeiros.SPRINGProject.Models.MusicRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@ExtendWith(MockitoExtension.class) +public class MusicControllerUpdateMusicByIdTest { + + @Mock + private MusicRepository MusicRepo; + + @Mock + private LogRepository Log; + + @InjectMocks + private MusicController controller; + + @Test + @Tag("valid") + public void testUpdateExistingMusicUpdatesAllFieldsAndReturnsConfirmation() { + // Arrange + final int id = 1; + final String newMusicName = "Updated Name"; + final String newMusicDescription = "Updated Description"; + final String newMusicContent = "Updated Content"; + MusicModel musicModel = mock(MusicModel.class); + when(MusicRepo.findById(id)).thenReturn(musicModel); + // Act + String result = controller.updateMusicById(id, newMusicName, newMusicDescription, newMusicContent); + // Assert + assertEquals((String) "Música Alterada!", (String) result); + verify(MusicRepo, times(1)).findById(eq(id)); + verify(musicModel, times(1)).setMusic(eq(newMusicContent)); + verify(musicModel, times(1)).setMusicDescription(eq(newMusicDescription)); + verify(musicModel, times(1)).setMusicName(eq(newMusicName)); + verify(MusicRepo, times(1)).save(same(musicModel)); + } + + @Test + @Tag("invalid") + public void testUpdateMusicByIdWhenRecordNotFoundThrowsNullPointerException() { + // Arrange + final int id = 999; // TODO: Adjust if repository uses a different ID for + // non-existent records + final String musicName = "Name"; + final String musicDescription = "Description"; + final String musicContent = "Content"; + when(MusicRepo.findById(id)).thenReturn(null); + // Act + Assert + assertThrows(NullPointerException.class, + () -> controller.updateMusicById(id, musicName, musicDescription, musicContent)); + verify(MusicRepo, times(1)).findById(eq(id)); + verify(MusicRepo, never()).save(any(MusicModel.class)); + } + + @Test + @Tag("valid") + public void testUpdateMusicByIdWithEmptyStringsStillSavesAndReturnsConfirmation() { + // Arrange + final int id = 2; + final String emptyName = ""; + final String emptyDescription = ""; + final String emptyMusic = ""; + MusicModel musicModel = mock(MusicModel.class); + when(MusicRepo.findById(id)).thenReturn(musicModel); + // Act + String result = controller.updateMusicById(id, emptyName, emptyDescription, emptyMusic); + // Assert + assertEquals((String) "Música Alterada!", (String) result); + verify(MusicRepo, times(1)).findById(eq(id)); + verify(musicModel, times(1)).setMusic(eq(emptyMusic)); + verify(musicModel, times(1)).setMusicDescription(eq(emptyDescription)); + verify(musicModel, times(1)).setMusicName(eq(emptyName)); + verify(MusicRepo, times(1)).save(same(musicModel)); + } + + @Test + @Tag("boundary") + public void testUpdateMusicByIdWithBoundaryIdZero() { + // Arrange + final int id = 0; + final String musicName = "Zero ID Name"; + final String musicDescription = "Zero ID Description"; + final String musicContent = "Zero ID Content"; + MusicModel musicModel = mock(MusicModel.class); + when(MusicRepo.findById(id)).thenReturn(musicModel); + // Act + String result = controller.updateMusicById(id, musicName, musicDescription, musicContent); + // Assert + assertEquals((String) "Música Alterada!", (String) result); + verify(MusicRepo, times(1)).findById(eq(id)); + verify(musicModel, times(1)).setMusic(eq(musicContent)); + verify(musicModel, times(1)).setMusicDescription(eq(musicDescription)); + verify(musicModel, times(1)).setMusicName(eq(musicName)); + verify(MusicRepo, times(1)).save(same(musicModel)); + } + + @Test + @Tag("boundary") + public void testUpdateMusicByIdWithNegativeId() { + // Arrange + final int id = -1; + final String musicName = "Negative ID Name"; + final String musicDescription = "Negative ID Description"; + final String musicContent = "Negative ID Content"; + MusicModel musicModel = mock(MusicModel.class); + when(MusicRepo.findById(id)).thenReturn(musicModel); + // Act + String result = controller.updateMusicById(id, musicName, musicDescription, musicContent); + // Assert + assertEquals((String) "Música Alterada!", (String) result); + verify(MusicRepo, times(1)).findById(eq(id)); + verify(musicModel, times(1)).setMusic(eq(musicContent)); + verify(musicModel, times(1)).setMusicDescription(eq(musicDescription)); + verify(musicModel, times(1)).setMusicName(eq(musicName)); + verify(MusicRepo, times(1)).save(same(musicModel)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/PagesControllerMinhaPaginaTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/PagesControllerMinhaPaginaTest.java new file mode 100644 index 00000000..34dcf906 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/PagesControllerMinhaPaginaTest.java @@ -0,0 +1,127 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=minhaPagina_faf3e427f2 +ROOST_METHOD_SIG_HASH=minhaPagina_b5557d79c5 + + +Scenario 1: Returns a non-null ModelAndView + +Details: + TestName: returnsNonNullModelAndView + Description: Verify that calling minhaPagina returns a non-null ModelAndView instance, ensuring the controller action always produces a response object. + +Execution: + Arrange: Instantiate PagesController (with or without dependency injection for LogRepository). + Act: Invoke minhaPagina. + Assert: Use JUnit assertions to confirm the returned ModelAndView is not null (assertNotNull). + +Validation: + Ensure that the controller method reliably produces a ModelAndView. Since there is no conditional logic, a non-null result is expected every time, confirming the method’s basic contract. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Map; +import com.medeiros.SPRINGProject.Models.LogRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.servlet.ModelAndView; +import static org.mockito.Mockito.verifyNoInteractions; +import org.junit.jupiter.api.*; +import com.medeiros.SPRINGProject.Models.LogModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@ExtendWith(MockitoExtension.class) +public class PagesControllerMinhaPaginaTest { + + private PagesController controller; + + @Mock + private LogRepository logRepository; + + @BeforeEach + public void setUp() { + controller = new PagesController(); + } + + @Test + @Tag("valid") + public void testReturnsNonNullModelAndView() { + ModelAndView result = controller.minhaPagina(); + assertNotNull((Object) result); + } + + @Test + @Tag("valid") + public void testHasCorrectViewName() { + ModelAndView result = controller.minhaPagina(); + assertEquals((String) "minhaPagina", (String) result.getViewName()); + } + + @Test + @Tag("valid") + public void testModelContainsExpectedTituloAndMensagem() { + ModelAndView result = controller.minhaPagina(); + Map model = result.getModel(); + assertTrue((boolean) model.containsKey("titulo")); + assertTrue((boolean) model.containsKey("mensagem")); + assertEquals((String) "Minha Página", (String) model.get("titulo")); + assertEquals((String) "Bem-vindo à minha página!", (String) model.get("mensagem")); + } + + @Test + @Tag("boundary") + public void testReturnsDistinctInstancesOnMultipleCalls() { + ModelAndView first = controller.minhaPagina(); + ModelAndView second = controller.minhaPagina(); + assertNotSame((Object) first, (Object) second); + } + + @Test + @Tag("valid") + public void testAttributesAreNonEmptyStrings() { + ModelAndView result = controller.minhaPagina(); + Map model = result.getModel(); + Object tituloObj = model.get("titulo"); + Object mensagemObj = model.get("mensagem"); + assertNotNull((Object) tituloObj); + assertNotNull((Object) mensagemObj); + String titulo = (String) tituloObj; + String mensagem = (String) mensagemObj; + assertTrue((boolean) (titulo.trim().length() > 0)); + assertTrue((boolean) (mensagem.trim().length() > 0)); + } + + @Test + @Tag("valid") + public void testModelHasExactlyTwoAttributes() { + ModelAndView result = controller.minhaPagina(); + Map model = result.getModel(); + assertEquals((int) 2, (int) model.size()); + } + + @Test + @Tag("integration") + public void testDoesNotInteractWithExternalLogRepository() { + ModelAndView result = controller.minhaPagina(); + assertNotNull((Object) result); + verifyNoInteractions(logRepository); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerDeleteUserByIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerDeleteUserByIdTest.java new file mode 100644 index 00000000..bcba4635 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerDeleteUserByIdTest.java @@ -0,0 +1,144 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=deleteUserById_d65ace15d7 +ROOST_METHOD_SIG_HASH=deleteUserById_91ecd15d81 + +Scenario 1: Successfully deletes when ID is a valid numeric string + +Details: + TestName: deletesUserWhenValidNumericId + Description: Verifies that when a valid numeric string (e.g., "123") is provided, the method parses it, calls UserAccRepo.deleteById with the correct integer value, and returns the expected success message. + +Execution: + Arrange: Set up a UserAccountController with a mock UserAccRepository that does not throw on deleteById, and mock UserInfoRepository and LogRepository. + Act: Call deleteUserById with "123". + Assert: Check that the returned string equals "Conta Deletada" and that UserAccRepo.deleteById was invoked exactly once with 123. Also verify no interactions with UserInfoRepo or Log. + +Validation: + Confirms the primary happy path: valid input leads to a delete call and a fixed success message. Ensures the method uses only UserAccRepo for this operation and returns the literal Portuguese message as designed. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.ArgumentMatchers.anyInt; +import org.junit.jupiter.api.*; + +@ExtendWith(MockitoExtension.class) +public class UserAccountControllerDeleteUserByIdTest { + + @Mock + private UserAccRepository UserAccRepo; + + @Mock + private UserInfoRepository UserInfoRepo; + + @Mock + private LogRepository Log; + + @InjectMocks + private UserAccountController controller; + + @BeforeEach + public void setUp() { + // No initialization required as mocks are injected by MockitoExtension + } + + @Test + @Tag("valid") + public void testDeletesUserWhenValidNumericId() { + String inputId = "123"; + String result = controller.deleteUserById(inputId); + assertEquals((String) "Conta Deletada", (String) result); + verify(UserAccRepo, times(1)).deleteById(123); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("boundary") + public void testDeletesUserWhenZeroId() { + String inputId = "0"; + String result = controller.deleteUserById(inputId); + assertEquals((String) "Conta Deletada", (String) result); + verify(UserAccRepo, times(1)).deleteById(0); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("boundary") + public void testDeletesUserWhenNegativeId() { + String inputId = "-5"; // TODO: Adjust to desired negative test value if needed + String result = controller.deleteUserById(inputId); + assertEquals((String) "Conta Deletada", (String) result); + verify(UserAccRepo, times(1)).deleteById(-5); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("invalid") + public void testThrowsNumberFormatExceptionWhenNonNumeric() { + String inputId = "abc"; + assertThrows(NumberFormatException.class, () -> controller.deleteUserById(inputId)); + verify(UserAccRepo, never()).deleteById(anyInt()); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("invalid") + public void testThrowsNumberFormatExceptionWhenNull() { + String inputId = null; + assertThrows(NumberFormatException.class, () -> controller.deleteUserById(inputId)); + verify(UserAccRepo, never()).deleteById(anyInt()); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("invalid") + public void testThrowsNumberFormatExceptionWhenWithSpaces() { + String inputId = " 42 "; + assertThrows(NumberFormatException.class, () -> controller.deleteUserById(inputId)); + verify(UserAccRepo, never()).deleteById(anyInt()); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + + @Test + @Tag("boundary") + public void testThrowsNumberFormatExceptionWhenOutOfIntRange() { + String inputId = "2147483648"; // Integer.MAX_VALUE + 1 + assertThrows(NumberFormatException.class, () -> controller.deleteUserById(inputId)); + verify(UserAccRepo, never()).deleteById(anyInt()); + verifyNoInteractions(UserInfoRepo); + verifyNoInteractions(Log); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerFindUserByIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerFindUserByIdTest.java new file mode 100644 index 00000000..2aefb36d --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerFindUserByIdTest.java @@ -0,0 +1,560 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=findUserById_fca20bfdc9 +ROOST_METHOD_SIG_HASH=findUserById_46f9fcf424 + +Scenario 1: Valid numeric ID string returns the User_Credentials from the repository + +Details: + TestName: validNumericIdReturnsUserCredentials + Description: Verifies that when a valid numeric string ID (e.g., "42") is provided, the method parses it to an integer and returns exactly the User_Credentials object provided by UserAccRepo.findById. + +Execution: + Arrange: Set up UserAccRepo as a mock to return a specific User_Credentials instance (e.g., mockUser) when called with 42. Ensure UserInfoRepo and Log are present as mocks/dummies but not interacted with. + Act: Call findUserById with "42". + Assert: Use assertions to confirm the returned object is the same instance as mockUser and that no exception is thrown. Verify UserAccRepo.findById is invoked exactly once with 42 and that there are no interactions with UserInfoRepo or Log. + +Validation: + Confirms correct parsing of a standard numeric string and proper delegation to the repository. Ensures the controller does not mutate or wrap the repository’s return value and avoids unintended interactions with unrelated components. + +Scenario 2: Nonexistent user (repository returns null) results in null return + +Details: + TestName: nonexistentUserReturnsNull + Description: Ensures that if UserAccRepo.findById returns null for a valid parsed integer ID, the method returns null directly. + +Execution: + Arrange: Mock UserAccRepo.findById(100) to return null. + Act: Call findUserById with "100". + Assert: Assert that the returned value is null and that no exception is thrown. Verify findById is called once with 100 and no interactions occur with UserInfoRepo or Log. + +Validation: + Verifies pass-through behavior for missing data and that the method does not attempt to handle absence beyond returning null, consistent with the current implementation. + +Scenario 3: ID with leading zeros is parsed correctly and repository is called with the parsed int + +Details: + TestName: idWithLeadingZerosParsesToInt + Description: Confirms that a numeric string with leading zeros (e.g., "0005") is parsed to the integer 5 and that the repository is called with 5. + +Execution: + Arrange: Mock UserAccRepo.findById(5) to return a known User_Credentials instance (mockUser). + Act: Call findUserById with "0005". + Assert: Assert that the returned object is mockUser and that UserAccRepo.findById received 5. Assert no other components are interacted with. + +Validation: + Ensures robust parsing behavior consistent with Integer.parseInt and correct argument propagation to the repository. + +Scenario 4: ID with a leading plus sign parses correctly + +Details: + TestName: plusSignedIdParsesCorrectly + Description: Validates that an ID like "+10" is accepted by Integer.parseInt and results in a repository call with 10. + +Execution: + Arrange: Mock UserAccRepo.findById(10) to return a User_Credentials instance (mockUser). + Act: Call findUserById with "+10". + Assert: Assert that the returned object is mockUser, and verify the repository was called with 10 exactly once. No interactions with UserInfoRepo or Log. + +Validation: + Confirms support for optional leading plus sign as allowed by Integer.parseInt. + +Scenario 5: ID equal to zero is handled by parsing and repository invocation + +Details: + TestName: zeroIdHandled + Description: Checks that "0" parses to 0 and the repository is invoked with 0, returning whatever the repository provides. + +Execution: + Arrange: Mock UserAccRepo.findById(0) to return a specific User_Credentials instance (mockUser). + Act: Call findUserById with "0". + Assert: Assert the returned object is mockUser and verify findById was called with 0. No other interactions. + +Validation: + Verifies acceptance of boundary value 0 and correct handoff to the repository. + +Scenario 6: Maximum int value as string is handled correctly + +Details: + TestName: maxIntIdHandled + Description: Ensures that "2147483647" (Integer.MAX_VALUE) is parsed successfully and used to call the repository. + +Execution: + Arrange: Mock UserAccRepo.findById(2147483647) to return mockUser. + Act: Call findUserById with "2147483647". + Assert: Assert the returned object is mockUser, and verify the repository was called once with 2147483647. No interactions with other components. + +Validation: + Confirms correct boundary handling at the upper limit of int. + +Scenario 7: Minimum int value (negative boundary) is handled correctly + +Details: + TestName: minIntIdHandled + Description: Ensures that "-2147483648" (Integer.MIN_VALUE) parses and the repository is called with that exact value. + +Execution: + Arrange: Mock UserAccRepo.findById(-2147483648) to return either a mock user or null, and set expected outcome accordingly. + Act: Call findUserById with "-2147483648". + Assert: Assert the result matches the repository’s configured return (either the mock user or null). Verify findById was called with -2147483648. + +Validation: + Confirms that negative boundary values are supported and properly delegated. + +Scenario 8: Purely non-numeric string results in NumberFormatException + +Details: + TestName: nonNumericIdThrowsNumberFormatException + Description: Verifies that providing an ID like "abc" triggers NumberFormatException from Integer.parseInt and the repository is not invoked. + +Execution: + Arrange: Ensure no repository stubbing for this input. + Act: Call findUserById with "abc". + Assert: Assert that NumberFormatException is thrown. Assert no interactions with UserAccRepo, UserInfoRepo, or Log. + +Validation: + Ensures input validation via parsing behaves as expected for non-numeric values. + +Scenario 9: Alphanumeric string results in NumberFormatException + +Details: + TestName: alphanumericIdThrowsNumberFormatException + Description: Confirms that "123abc" is rejected by the parser and the method throws NumberFormatException without calling the repository. + +Execution: + Arrange: No repository stubs are needed. + Act: Call findUserById with "123abc". + Assert: Assert that NumberFormatException is thrown. Verify no interactions with any repositories or Log. + +Validation: + Enforces strict numeric formatting requirements. + +Scenario 10: Empty string results in NumberFormatException + +Details: + TestName: emptyIdThrowsNumberFormatException + Description: An empty ID string should cause Integer.parseInt to throw NumberFormatException, preventing any repository calls. + +Execution: + Arrange: No repository stubs are needed. + Act: Call findUserById with "". + Assert: Assert NumberFormatException is thrown and verify no interactions with UserAccRepo, UserInfoRepo, or Log. + +Validation: + Confirms enforcement of non-empty, numeric input. + +Scenario 11: Whitespace-only string results in NumberFormatException + +Details: + TestName: whitespaceOnlyIdThrowsNumberFormatException + Description: A string containing only whitespace (e.g., " ") is invalid and should cause NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Call findUserById with " ". + Assert: Assert NumberFormatException is thrown and no repository interactions occur. + +Validation: + Ensures trimming is not performed and pure whitespace is treated as invalid input. + +Scenario 12: Null ID results in NumberFormatException + +Details: + TestName: nullIdThrowsNumberFormatException + Description: Passing a null ID to Integer.parseInt should raise NumberFormatException ("null"). + +Execution: + Arrange: No repository stubbing required. + Act: Call findUserById with null. + Assert: Assert NumberFormatException is thrown. Verify no interactions with UserAccRepo, UserInfoRepo, or Log. + +Validation: + Verifies that null inputs are not accepted and parsing enforces input presence. + +Scenario 13: Overflow beyond Integer.MAX_VALUE results in NumberFormatException + +Details: + TestName: overflowIdThrowsNumberFormatException + Description: Providing "2147483648" (MAX_VALUE + 1) should cause NumberFormatException due to overflow. + +Execution: + Arrange: No repository setup needed. + Act: Call findUserById with "2147483648". + Assert: Assert NumberFormatException is thrown and confirm no repository calls. + +Validation: + Ensures numeric boundary enforcement and safe failure on overflow. + +Scenario 14: Underflow beyond Integer.MIN_VALUE results in NumberFormatException + +Details: + TestName: underflowIdThrowsNumberFormatException + Description: Providing "-2147483649" (MIN_VALUE - 1) should cause NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Call findUserById with "-2147483649". + Assert: Assert NumberFormatException is thrown and verify no repository interaction. + +Validation: + Confirms enforcement of lower integer boundary conditions. + +Scenario 15: Non-ASCII digit string results in NumberFormatException + +Details: + TestName: nonAsciiDigitIdThrowsNumberFormatException + Description: A string like "123" (full-width digits) is not recognized by Integer.parseInt and should cause NumberFormatException. + +Execution: + Arrange: No repository setup needed. + Act: Call findUserById with "123". + Assert: Assert NumberFormatException is thrown. Confirm no calls to UserAccRepo. + +Validation: + Ensures inputs must be standard ASCII numeric characters. + +Scenario 16: Hex-prefixed string is rejected with NumberFormatException + +Details: + TestName: hexPrefixedIdThrowsNumberFormatException + Description: A string like "0x10" should not be interpreted as a valid decimal number and should cause NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Call findUserById with "0x10". + Assert: Assert NumberFormatException is thrown and no repository calls are made. + +Validation: + Confirms decimal-only parsing and rejection of non-decimal formats. + +Scenario 17: String with trailing newline results in NumberFormatException + +Details: + TestName: idWithTrailingNewlineThrowsNumberFormatException + Description: A string such as "5\n" should fail parsing and raise NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Call findUserById with "5\n". + Assert: Assert NumberFormatException is thrown. Verify no repository interactions occur. + +Validation: + Confirms that extraneous whitespace/control characters render the input invalid. + +Scenario 18: Double sign prefix results in NumberFormatException + +Details: + TestName: doubleSignIdThrowsNumberFormatException + Description: A string like "++1" is invalid and should cause NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Call findUserById with "++1". + Assert: Assert NumberFormatException is thrown and no repository interactions occur. + +Validation: + Ensures strict adherence to valid sign formatting for numeric strings. + +Scenario 19: Repository runtime exception is propagated unchanged + +Details: + TestName: repositoryThrowsRuntimeExceptionPropagates + Description: If UserAccRepo.findById throws a RuntimeException (e.g., data access failure), the controller method should not catch it and should propagate the exception. + +Execution: + Arrange: Mock UserAccRepo.findById(7) to throw a RuntimeException. + Act: Call findUserById with "7". + Assert: Assert that the same RuntimeException is thrown and not wrapped. Verify findById was attempted exactly once. + +Validation: + Confirms that the method does not suppress or alter exceptions from the repository, preserving error semantics. + +Scenario 20: No interaction with UserInfoRepo and Log for find operation + +Details: + TestName: noInteractionWithOtherRepositories + Description: Ensures that findUserById never interacts with UserInfoRepo or Log, regardless of successful or failing parse. + +Execution: + Arrange: Provide mocks for UserInfoRepo and Log. Set UserAccRepo.findById(9) to return a mock user. + Act: Call findUserById with "9". + Assert: Assert the returned object is the mock user. Verify zero interactions with UserInfoRepo and Log. Optionally, repeat with an invalid input (e.g., "x") and verify that neither is touched. + +Validation: + Ensures the method’s scope is limited to user credential retrieval and does not perform unrelated operations. + +Scenario 21: Extremely long numeric string causes NumberFormatException + +Details: + TestName: extremelyLongNumericStringThrowsNumberFormatException + Description: An excessively long numeric string (e.g., 1000 digits) should not parse successfully and should cause NumberFormatException. + +Execution: + Arrange: Generate a long numeric string and avoid stubbing the repository. + Act: Call findUserById with the long string. + Assert: Assert NumberFormatException is thrown. Verify no repository calls. + +Validation: + Confirms robust failure behavior under unusually large inputs that exceed int capacity. + +Scenario 22: Negative zero string parses to zero and calls repository with 0 + +Details: + TestName: negativeZeroIdParsesToZero + Description: Validates that "-0" is parsed by Integer.parseInt as 0 and that the repository receives 0. + +Execution: + Arrange: Mock UserAccRepo.findById(0) to return a known User_Credentials instance (mockUser). + Act: Call findUserById with "-0". + Assert: Assert the returned object is mockUser and verify findById is called with 0 exactly once. No interactions with other components. + +Validation: + Ensures consistent parsing behavior for signed zero inputs and correct delegation to the repository. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.*; + +@ExtendWith(MockitoExtension.class) +public class UserAccountControllerFindUserByIdTest { + + @Mock + private UserAccRepository UserAccRepo; + + @Mock + private UserInfoRepository UserInfoRepo; + + @Mock + private LogRepository Log; + + @InjectMocks + private UserAccountController controller; + + @Tag("valid") + @Test + public void validNumericIdReturnsUserCredentials() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(42)).thenReturn(mockUser); + User_Credentials result = assertDoesNotThrow(() -> controller.findUserById("42")); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(42); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("valid") + @Test + public void nonexistentUserReturnsNull() { + when(UserAccRepo.findById(100)).thenReturn(null); + User_Credentials result = controller.findUserById("100"); + assertNull((Object) result); + verify(UserAccRepo, times(1)).findById(100); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("valid") + @Test + public void idWithLeadingZerosParsesToInt() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(5)).thenReturn(mockUser); + User_Credentials result = controller.findUserById("0005"); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(5); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("valid") + @Test + public void plusSignedIdParsesCorrectly() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(10)).thenReturn(mockUser); + User_Credentials result = controller.findUserById("+10"); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(10); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("boundary") + @Test + public void zeroIdHandled() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(0)).thenReturn(mockUser); + User_Credentials result = controller.findUserById("0"); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(0); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("boundary") + @Test + public void maxIntIdHandled() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(2147483647)).thenReturn(mockUser); + User_Credentials result = controller.findUserById("2147483647"); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(2147483647); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("boundary") + @Test + public void minIntIdHandled() { + when(UserAccRepo.findById(-2147483648)).thenReturn(null); + User_Credentials result = controller.findUserById("-2147483648"); + assertNull((Object) result); + verify(UserAccRepo, times(1)).findById(-2147483648); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void nonNumericIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("abc")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void alphanumericIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("123abc")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void emptyIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void whitespaceOnlyIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById(" ")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void nullIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById(null)); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void overflowIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("2147483648")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void underflowIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("-2147483649")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void nonAsciiDigitIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("123")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void hexPrefixedIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("0x10")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void idWithTrailingNewlineThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("5\n")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void doubleSignIdThrowsNumberFormatException() { + assertThrows(NumberFormatException.class, () -> controller.findUserById("++1")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("valid") + @Test + public void repositoryThrowsRuntimeExceptionPropagates() { + RuntimeException ex = new RuntimeException("Data access failure"); // TODO Change + // the message + // if needed + // based on + // repository + // exception + // types + when(UserAccRepo.findById(7)).thenThrow(ex); + RuntimeException thrown = assertThrows(RuntimeException.class, () -> controller.findUserById("7")); + assertSame((Object) ex, (Object) thrown); + verify(UserAccRepo, times(1)).findById(7); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("valid") + @Test + public void noInteractionWithOtherRepositories() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(9)).thenReturn(mockUser); + User_Credentials result = controller.findUserById("9"); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(9); + verifyNoInteractions(UserInfoRepo, Log); + assertThrows(NumberFormatException.class, () -> controller.findUserById("x")); + verifyNoMoreInteractions(UserAccRepo); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Tag("invalid") + @Test + public void extremelyLongNumericStringThrowsNumberFormatException() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append('1'); + } + String longNumeric = sb.toString(); + assertThrows(NumberFormatException.class, () -> controller.findUserById(longNumeric)); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Tag("boundary") + @Test + public void negativeZeroIdParsesToZero() { + User_Credentials mockUser = mock(User_Credentials.class); + when(UserAccRepo.findById(0)).thenReturn(mockUser); + User_Credentials result = controller.findUserById("-0"); + assertSame((Object) mockUser, (Object) result); + verify(UserAccRepo, times(1)).findById(0); + verifyNoInteractions(UserInfoRepo, Log); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerUpdateInfoUserByTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerUpdateInfoUserByTest.java new file mode 100644 index 00000000..32aea883 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerUpdateInfoUserByTest.java @@ -0,0 +1,149 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=updateInfoUserBy_b88001e4be +ROOST_METHOD_SIG_HASH=updateInfoUserBy_642c0765ab + +Scenario 1: Successful save returns confirmation and persists new user info + +Details: + TestName: updateInfoSavesAndReturnsUpdated + Description: Verifies the happy-path behavior where valid inputs lead to creation of a User_Info instance, persistence via UserInfoRepo.save, and the controller returns the confirmation string "ATUALIZADO". + +Execution: + Arrange: Mock UserInfoRepository to accept any User_Info instance without throwing exceptions. Instantiate UserAccountController with the mocked UserInfoRepo; other repositories can be mocked or left unused. + Act: Invoke updateInfoUserBy with a valid userId and representative non-empty strings for all parameters (photoURL, favoritesMusics, gender, phone, instaURL, twitterURL, favoritesThings). + Assert: Use a JUnit assertion to check that the returned string equals "ATUALIZADO". Verify that UserInfoRepo.save was called exactly once with an instance of User_Info. + +Validation: + The assertion verifies that the controller responds with the expected success message and that the persistence operation is attempted once. This confirms that the method’s core behavior (constructing and saving User_Info, then returning "ATUALIZADO") works under normal conditions. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +public class UserAccountControllerUpdateInfoUserByTest { + + @Mock + private UserInfoRepository UserInfoRepo; + + @InjectMocks + private UserAccountController controller; + + @Test + @Tag("valid") + public void testUpdateInfoSavesAndReturnsUpdated() { + int userId = 123; // TODO: Adjust sample userId if needed + String photoURL = "http://example.com/photo.jpg"; // TODO: Adjust sample URLs and + // data as needed + String favoritesMusics = "rock,pop"; + String gender = "non-binary"; + String phone = "+1234567890"; + String instaURL = "http://instagram.com/user"; + String twitterURL = "http://twitter.com/user"; + String favoritesThings = "coding,reading"; + ArgumentCaptor captor = ArgumentCaptor.forClass(User_Info.class); + String result = controller.updateInfoUserBy(userId, photoURL, favoritesMusics, gender, phone, instaURL, + twitterURL, favoritesThings); + assertEquals((String) "ATUALIZADO", (String) result); + verify(UserInfoRepo, times(1)).save(captor.capture()); + Object savedUserInfo = captor.getValue(); + assertNotNull((Object) savedUserInfo); + verifyNoMoreInteractions(UserInfoRepo); + } + + @Test + @Tag("boundary") + public void testUpdateInfoWithEmptyStringsReturnsUpdated() { + int userId = 1; + String photoURL = ""; + String favoritesMusics = ""; + String gender = ""; + String phone = ""; + String instaURL = ""; + String twitterURL = ""; + String favoritesThings = ""; + String result = controller.updateInfoUserBy(userId, photoURL, favoritesMusics, gender, phone, instaURL, + twitterURL, favoritesThings); + assertEquals((String) "ATUALIZADO", (String) result); + verify(UserInfoRepo, times(1)).save(any(User_Info.class)); + verifyNoMoreInteractions(UserInfoRepo); + } + + @Test + @Tag("boundary") + public void testUpdateInfoWithZeroUserIdReturnsUpdated() { + int userId = 0; + String photoURL = "http://example.com/p.jpg"; + String favoritesMusics = "jazz"; + String gender = "unspecified"; + String phone = "0000000000"; + String instaURL = "http://instagram.com/zero"; + String twitterURL = "http://twitter.com/zero"; + String favoritesThings = "none"; + String result = controller.updateInfoUserBy(userId, photoURL, favoritesMusics, gender, phone, instaURL, + twitterURL, favoritesThings); + assertEquals((String) "ATUALIZADO", (String) result); + verify(UserInfoRepo, times(1)).save(any(User_Info.class)); + verifyNoMoreInteractions(UserInfoRepo); + } + + @Test + @Tag("boundary") + public void testUpdateInfoWithMaxIntUserIdReturnsUpdated() { + int userId = Integer.MAX_VALUE; + String photoURL = "http://example.com/photo_max.jpg"; + String favoritesMusics = "classical"; + String gender = "prefer-not-to-say"; + String phone = "+19876543210"; + String instaURL = "http://instagram.com/maxuser"; + String twitterURL = "http://twitter.com/maxuser"; + String favoritesThings = "hiking,photography"; + String result = controller.updateInfoUserBy(userId, photoURL, favoritesMusics, gender, phone, instaURL, + twitterURL, favoritesThings); + assertEquals((String) "ATUALIZADO", (String) result); + verify(UserInfoRepo, times(1)).save(any(User_Info.class)); + verifyNoMoreInteractions(UserInfoRepo); + } + + @Test + @Tag("invalid") + public void testUpdateInfoWhenRepositoryThrowsExceptionPropagates() { + int userId = 10; + String photoURL = "http://example.com/error.jpg"; + String favoritesMusics = "blues"; + String gender = "male"; + String phone = "+1112223333"; + String instaURL = "http://instagram.com/erroruser"; + String twitterURL = "http://twitter.com/erroruser"; + String favoritesThings = "testing"; + doThrow(new RuntimeException("save failed")).when(UserInfoRepo).save(any(User_Info.class)); + assertThrows(RuntimeException.class, () -> controller.updateInfoUserBy(userId, photoURL, favoritesMusics, + gender, phone, instaURL, twitterURL, favoritesThings)); + verify(UserInfoRepo, times(1)).save(any(User_Info.class)); + verifyNoMoreInteractions(UserInfoRepo); + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerUpdateUserByIdTest.java b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerUpdateUserByIdTest.java new file mode 100644 index 00000000..f5c015c9 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/Controllers/UserAccountControllerUpdateUserByIdTest.java @@ -0,0 +1,810 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=updateUserById_45a90e9627 +ROOST_METHOD_SIG_HASH=updateUserById_9cf5a84c94 + +Scenario 1: Successful update when user exists + +Details: + TestName: updateExistingUserSucceedsAndSaves + Description: Verifies that when a valid numeric id is provided and UserAccRepository.findById returns a non-null User_Credentials, the method updates email, password, and username, saves the entity, and returns the success message. + +Execution: + Arrange: Mock UserAccRepository.findById to return a non-null User_Credentials for the given parsed integer id. Prepare sample values for email, password, and username. + Act: Invoke updateUserById with a valid id (e.g., "123"), and non-empty email, password, and username. + Assert: Assert that the returned String equals "Usuário Salvo". Verify that setEmail, setPassword, and setUsername on the returned User_Credentials were called with the provided values, and UserAccRepository.save was called exactly once with that same instance. Verify that UserInfoRepository and LogRepository were not interacted with. + +Validation: + Confirms that the happy path updates and persists the user record and returns the precise success message. Ensures no unintended side effects occur on other repositories. + +Scenario 2: Not found when repository returns null + +Details: + TestName: returnsNotFoundMessageWhenUserDoesNotExist + Description: Ensures that when UserAccRepository.findById returns null for a valid numeric id, the method does not attempt to update or save and returns the not-found message. + +Execution: + Arrange: Mock UserAccRepository.findById to return null for the parsed integer id. + Act: Invoke updateUserById with a valid id string whose parsed value maps to the mocked null result. + Assert: Assert that the returned String equals "User não encontrado". Verify that setEmail, setPassword, setUsername, and UserAccRepository.save are never called. Verify no interactions with UserInfoRepository and LogRepository. + +Validation: + Confirms correct handling of a missing user and validates no updates or persistence occur in this branch. + +Scenario 3: Non-numeric id causes NumberFormatException + +Details: + TestName: throwsNumberFormatExceptionWhenIdIsNonNumeric + Description: Checks that a non-numeric id (e.g., "abc") causes Integer.parseInt to throw NumberFormatException and that the repository is not called. + +Execution: + Arrange: No repository setup required. + Act: Invoke updateUserById with id as "abc" and valid email, password, username. + Assert: Expect a NumberFormatException to be thrown. Verify that UserAccRepository.findById and UserAccRepository.save are never called. Verify no interactions with UserInfoRepository and LogRepository. + +Validation: + Ensures input validation via parseInt is enforced implicitly and that the method fails fast on invalid id input. + +Scenario 4: Null id causes NullPointerException + +Details: + TestName: throwsNullPointerExceptionWhenIdIsNull + Description: Verifies behavior when id is null, which will trigger a NullPointerException at Integer.parseInt before any repository interaction. + +Execution: + Arrange: No repository setup required. + Act: Invoke updateUserById with id as null and valid email, password, username. + Assert: Expect a NullPointerException to be thrown. Verify no interactions with UserAccRepository, UserInfoRepository, and LogRepository. + +Validation: + Confirms that null ids are not handled and result in an exception, matching current method behavior. + +Scenario 5: Out-of-range id causes NumberFormatException + +Details: + TestName: throwsNumberFormatExceptionWhenIdOutOfIntRange + Description: Validates that an id string outside the integer range (e.g., "999999999999") results in NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Invoke updateUserById with a very large numeric string out of int range. + Assert: Expect NumberFormatException. Verify no interactions with repositories. + +Validation: + Ensures the method correctly fails on input that cannot be parsed into a 32-bit integer. + +Scenario 6: Id with whitespace fails parsing + +Details: + TestName: throwsNumberFormatExceptionWhenIdHasSurroundingWhitespace + Description: Confirms that id values with leading/trailing spaces (e.g., " 123 ") are not trimmed and cause NumberFormatException. + +Execution: + Arrange: No repository setup required. + Act: Invoke updateUserById with id as " 123 " and valid other parameters. + Assert: Expect NumberFormatException. Verify no repository interactions. + +Validation: + Shows that the method does not normalize whitespace around id and fails fast on such input. + +Scenario 7: Id with leading zeros parses and updates + +Details: + TestName: updatesSuccessfullyWhenIdHasLeadingZeros + Description: Ensures that an id like "000123" is parsed to 123 and, if the user exists, the update proceeds as normal. + +Execution: + Arrange: Mock UserAccRepository.findById(123) to return a non-null user. + Act: Invoke updateUserById with id "000123" and valid email, password, username. + Assert: Assert returned value is "Usuário Salvo". Verify setters invoked with provided values and save called once with the same instance. + +Validation: + Confirms that leading zeros do not affect integer parsing and normal update flow proceeds. + +Scenario 8: Id with a leading plus sign parses and updates + +Details: + TestName: updatesSuccessfullyWhenIdHasLeadingPlusSign + Description: Verifies that "+42" parses to 42 and the method updates and saves when the user exists. + +Execution: + Arrange: Mock UserAccRepository.findById(42) to return a non-null user. + Act: Invoke updateUserById with id "+42". + Assert: Confirm "Usuário Salvo" is returned and setters/save were executed as expected. + +Validation: + Demonstrates tolerant parsing of signed integers and successful update behavior. + +Scenario 9: Negative id not found path + +Details: + TestName: returnsNotFoundWhenNegativeIdResolvesToNoUser + Description: Checks behavior when id is negative (e.g., "-1") and the repository returns null, leading to the not-found message. + +Execution: + Arrange: Mock UserAccRepository.findById(-1) to return null. + Act: Invoke updateUserById with id "-1". + Assert: Assert returned value is "User não encontrado". Verify no setter or save calls. + +Validation: + Ensures negative ids that do not map to a user are treated as not found. + +Scenario 10: Negative id but user exists still updates + +Details: + TestName: updatesSuccessfullyEvenWithNegativeIdIfUserExists + Description: Shows that the method does not validate id sign; if the repository returns a user for a negative id, it proceeds to update and save. + +Execution: + Arrange: Mock UserAccRepository.findById(-7) to return a non-null user. + Act: Invoke updateUserById with id "-7" and valid fields. + Assert: Assert "Usuário Salvo" is returned and setters/save invoked once. + +Validation: + Highlights that business rules for id sign are not enforced within this method; behavior depends solely on repository result. + +Scenario 11: Null email is accepted and saved + +Details: + TestName: acceptsNullEmailAndSavesWhenUserExists + Description: Validates that email can be null; the method sets null on the entity and still saves successfully. + +Execution: + Arrange: Mock UserAccRepository.findById to return a non-null user. + Act: Invoke updateUserById with email as null, and valid id, password, username. + Assert: Assert "Usuário Salvo" is returned. Verify setEmail(null) invoked, along with setPassword and setUsername, then save called once. + +Validation: + Confirms lack of input validation for email and that null values are persisted as provided. + +Scenario 12: Empty username is accepted and saved + +Details: + TestName: acceptsEmptyUsernameAndSavesWhenUserExists + Description: Ensures that an empty string for username is set and saved without validation. + +Execution: + Arrange: Mock repository to return a user for the parsed id. + Act: Call updateUserById with username as "" and valid id, email, password. + Assert: Assert "Usuário Salvo". Verify setUsername("") called and save executed. + +Validation: + Demonstrates that the method does not enforce non-empty username constraints. + +Scenario 13: Empty password is accepted and saved + +Details: + TestName: acceptsEmptyPasswordAndSavesWhenUserExists + Description: Ensures that an empty password is set and saved, indicating no validation or hashing in this method. + +Execution: + Arrange: Mock repository to return a user. + Act: Invoke updateUserById with password as "" and valid id, email, username. + Assert: Assert "Usuário Salvo". Verify setPassword("") and save called once. + +Validation: + Highlights potential security implications: passwords are accepted verbatim; method does not hash or validate. + +Scenario 14: Very long strings are persisted as-is + +Details: + TestName: persistsVeryLongFieldsWithoutValidation + Description: Confirms the method can handle very long email, password, and username strings and still save successfully. + +Execution: + Arrange: Mock repository to return a user. Prepare very long strings for email/password/username (within memory limits). + Act: Invoke updateUserById with those long strings. + Assert: Assert "Usuário Salvo". Verify setters called with the exact long values and save invoked. + +Validation: + Ensures the method does not truncate or validate string lengths and persists data as provided. + +Scenario 15: Unicode characters are accepted and saved + +Details: + TestName: acceptsUnicodeCharactersInFields + Description: Verifies that fields containing Unicode (e.g., "José", "пароль", "用户@example.com") are set and saved without alteration. + +Execution: + Arrange: Mock repository to return a user. + Act: Invoke updateUserById with Unicode-containing email, password, and username. + Assert: Assert "Usuário Salvo". Verify setters called with the provided Unicode values and save invoked once. + +Validation: + Confirms internationalization tolerance in raw string handling for updates. + +Scenario 16: Special characters and quotes are preserved + +Details: + TestName: preservesSpecialCharactersAndQuotesInFields + Description: Ensures that values containing quotes, symbols, and escape characters (e.g., "name\"with\"quotes", "pass!@#$%^&*()", "email+tag@example.com") are set and saved unchanged. + +Execution: + Arrange: Mock repository to return a user. + Act: Invoke updateUserById with fields containing special characters. + Assert: Assert "Usuário Salvo". Verify setters called with exact values and save invoked. + +Validation: + Confirms that the method performs no sanitization or normalization and persists inputs verbatim. + +Scenario 17: Repository findById throws an exception + +Details: + TestName: propagatesExceptionWhenFindByIdFails + Description: Validates that if UserAccRepository.findById throws a runtime exception (e.g., database error), the method propagates the exception without returning a message. + +Execution: + Arrange: Configure UserAccRepository.findById to throw a RuntimeException. + Act: Invoke updateUserById with a valid id and other fields. + Assert: Expect the same RuntimeException to be thrown. Verify no calls to setters or save occurred. Verify no interactions with UserInfoRepository and LogRepository. + +Validation: + Confirms absence of error handling; exceptions from repository are not swallowed. + +Scenario 18: Repository save throws an exception + +Details: + TestName: propagatesExceptionWhenSaveFailsAfterUpdates + Description: Ensures that if UserAccRepository.save throws a runtime exception after setters have been called, the exception is propagated and no success message is returned. + +Execution: + Arrange: Mock findById to return a user and configure save to throw a RuntimeException. + Act: Invoke updateUserById with valid inputs. + Assert: Expect the RuntimeException. Verify setters were called before the exception, and save was attempted once. + +Validation: + Confirms that persistence errors are surfaced to the caller and that updates occur prior to the save attempt. + +Scenario 19: Verifies exact success message including diacritics + +Details: + TestName: returnsExactSuccessMessageWithDiacritics + Description: Focuses on verifying the exact returned string "Usuário Salvo" including correct capitalization and diacritics when update succeeds. + +Execution: + Arrange: Mock findById to return a user. + Act: Invoke updateUserById with valid inputs. + Assert: Assert that the returned value is exactly "Usuário Salvo" (no extra spaces or different accents). + +Validation: + Ensures clients depending on exact response text will behave correctly. + +Scenario 20: Verifies exact not-found message text + +Details: + TestName: returnsExactNotFoundMessage + Description: Validates that the method returns exactly "User não encontrado" when the repository returns null. + +Execution: + Arrange: Mock findById to return null. + Act: Invoke updateUserById with a valid numeric id string. + Assert: Assert that the returned value is exactly "User não encontrado". + +Validation: + Confirms response text consistency for not-found scenarios. + +Scenario 21: Ensures parsed id passed correctly to repository + +Details: + TestName: passesParsedIntegerIdToRepository + Description: Checks that the integer value resulting from parsing the id string is used in the repository call (e.g., "00123" -> 123). + +Execution: + Arrange: Prepare a spy or verification on UserAccRepository to capture the id argument. Mock findById(123) to return a user. + Act: Invoke updateUserById with id "00123". + Assert: Verify that findById was called with 123 and not the original string. Assert "Usuário Salvo" is returned. + +Validation: + Confirms that id parsing is applied as intended and correct numeric id is used for lookup. + +Scenario 22: Ensures setters are called before save + +Details: + TestName: callsSettersBeforeSave + Description: Verifies that setEmail, setPassword, and setUsername are invoked on the entity prior to calling UserAccRepository.save. + +Execution: + Arrange: Use a spy for User_Credentials returned by findById to verify call order; mock repository to return this spy. + Act: Invoke updateUserById with valid inputs. + Assert: Verify that the three setter methods are called with the provided values and that save is called after these method calls. + +Validation: + Ensures the entity state is updated before persistence, aligning with expected update semantics. + +Scenario 23: Whitespace-only fields are accepted and saved + +Details: + TestName: acceptsWhitespaceOnlyFields + Description: Ensures that strings containing only whitespace for email/username/password are accepted, set, and saved. + +Execution: + Arrange: Mock repository to return a user. + Act: Invoke updateUserById with fields like " " for email/username/password. + Assert: Assert "Usuário Salvo". Verify setters called with whitespace-only strings and save invoked. + +Validation: + Confirms the absence of input normalization or trimming in the update path. + +Scenario 24: No interaction with unused repositories on any path + +Details: + TestName: noInteractionWithUserInfoAndLogRepositories + Description: Validates that UserInfoRepository and LogRepository are not used by updateUserById in either success, not-found, or exception scenarios. + +Execution: + Arrange: Prepare mocks for UserInfoRepository and LogRepository. Create three sub-arrangements: (a) success path with findById returning user, (b) not-found with findById returning null, (c) error path with findById throwing an exception. + Act: Invoke updateUserById under each sub-arrangement. + Assert: In all cases, verify zero interactions with UserInfoRepository and LogRepository. + +Validation: + Ensures method cohesion and prevents unintended side effects in unrelated components. + +Scenario 25: Zero id path based on repository outcome + +Details: + TestName: handlesZeroIdDependingOnRepositoryResult + Description: Confirms that id "0" is parsed and behavior depends solely on repository return: updates if user exists; not found otherwise. + +Execution: + Arrange: Two sub-cases: (a) Mock findById(0) to return a user, (b) Mock findById(0) to return null. + Act: Invoke updateUserById with id "0" for both sub-cases. + Assert: (a) Expect "Usuário Salvo" with setters/save verified; (b) Expect "User não encontrado" with no setters/save. + +Validation: + Demonstrates that zero is treated as a valid id and the outcome is repository-dependent. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.Controllers; + +import com.medeiros.SPRINGProject.Models.*; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +public class UserAccountControllerUpdateUserByIdTest { + + @InjectMocks + private UserAccountController controller; + + @Mock + private UserAccRepository UserAccRepo; + + @Mock + private UserInfoRepository UserInfoRepo; + + @Mock + private LogRepository Log; + + @Test + @Tag("valid") + public void updateExistingUserSucceedsAndSaves() { + final String id = "123"; + final int parsedId = 123; + final String email = "test@example.com"; + final String password = "securePassword"; + final String username = "newUser"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user, times(1)).setEmail(email); + verify(user, times(1)).setPassword(password); + verify(user, times(1)).setUsername(username); + verify(UserAccRepo, times(1)).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void returnsNotFoundMessageWhenUserDoesNotExist() { + final String id = "123"; + final int parsedId = 123; + when(UserAccRepo.findById(parsedId)).thenReturn(null); + String result = controller.updateUserById(id, "email@example.com", "pwd", "user"); + assertEquals((String) "User não encontrado", (String) result); + verify(UserAccRepo, never()).save(any()); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void throwsNumberFormatExceptionWhenIdIsNonNumeric() { + final String id = "abc"; + assertThrows(NumberFormatException.class, () -> controller.updateUserById(id, "e", "p", "u")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void throwsNullPointerExceptionWhenIdIsNull() { + final String id = null; + assertThrows(NullPointerException.class, () -> controller.updateUserById(id, "e", "p", "u")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Test + @Tag("boundary") + public void throwsNumberFormatExceptionWhenIdOutOfIntRange() { + final String id = "999999999999"; + assertThrows(NumberFormatException.class, () -> controller.updateUserById(id, "e", "p", "u")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void throwsNumberFormatExceptionWhenIdHasSurroundingWhitespace() { + final String id = " 123 "; + assertThrows(NumberFormatException.class, () -> controller.updateUserById(id, "e", "p", "u")); + verifyNoInteractions(UserAccRepo, UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void updatesSuccessfullyWhenIdHasLeadingZeros() { + final String id = "000123"; + final int parsedId = 123; + final String email = "lead.zeros@example.com"; + final String password = "pwd123"; + final String username = "user123"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void updatesSuccessfullyWhenIdHasLeadingPlusSign() { + final String id = "+42"; + final int parsedId = 42; + final String email = "plus@example.com"; + final String password = "pwd"; + final String username = "userPlus"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void returnsNotFoundWhenNegativeIdResolvesToNoUser() { + final String id = "-1"; + final int parsedId = -1; + when(UserAccRepo.findById(parsedId)).thenReturn(null); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "User não encontrado", (String) result); + verify(UserAccRepo, never()).save(any()); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void updatesSuccessfullyEvenWithNegativeIdIfUserExists() { + final String id = "-7"; + final int parsedId = -7; + final String email = "neg@example.com"; + final String password = "pass"; + final String username = "negUser"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void acceptsNullEmailAndSavesWhenUserExists() { + final String id = "10"; + final int parsedId = 10; + final String email = null; + final String password = "pwd"; + final String username = "user"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void acceptsEmptyUsernameAndSavesWhenUserExists() { + final String id = "11"; + final int parsedId = 11; + final String email = "user@example.com"; + final String password = "pwd"; + final String username = ""; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void acceptsEmptyPasswordAndSavesWhenUserExists() { + final String id = "12"; + final int parsedId = 12; + final String email = "user@example.com"; + final String password = ""; + final String username = "user"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("boundary") + public void persistsVeryLongFieldsWithoutValidation() { + final String id = "13"; + final int parsedId = 13; + // TODO: Adjust length based on environment constraints if needed + final int longLength = 5000; + final String longEmail = "e".repeat(longLength) + "@example.com"; + final String longPassword = "p".repeat(longLength); + final String longUsername = "u".repeat(longLength); + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, longEmail, longPassword, longUsername); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(longEmail); + verify(user).setPassword(longPassword); + verify(user).setUsername(longUsername); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void acceptsUnicodeCharactersInFields() { + final String id = "14"; + final int parsedId = 14; + final String email = "用户@example.com"; + final String password = "пароль"; + final String username = "José"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void preservesSpecialCharactersAndQuotesInFields() { + final String id = "15"; + final int parsedId = 15; + final String email = "email+tag@example.com"; + final String password = "pass!@#$%^&*()"; + final String username = "name\"with\"quotes"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void propagatesExceptionWhenFindByIdFails() { + final String id = "16"; + final int parsedId = 16; + when(UserAccRepo.findById(parsedId)).thenThrow(new RuntimeException("DB error")); + assertThrows(RuntimeException.class, () -> controller.updateUserById(id, "e", "p", "u")); + verify(UserAccRepo, never()).save(any()); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("invalid") + public void propagatesExceptionWhenSaveFailsAfterUpdates() { + final String id = "17"; + final int parsedId = 17; + final String email = "e@example.com"; + final String password = "pwd"; + final String username = "user"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + doThrow(new RuntimeException("Save error")).when(UserAccRepo).save(user); + assertThrows(RuntimeException.class, () -> controller.updateUserById(id, email, password, username)); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo, times(1)).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("valid") + public void returnsExactSuccessMessageWithDiacritics() { + final String id = "18"; + final int parsedId = 18; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "Usuário Salvo", (String) result); + } + + @Test + @Tag("invalid") + public void returnsExactNotFoundMessage() { + final String id = "19"; + final int parsedId = 19; + when(UserAccRepo.findById(parsedId)).thenReturn(null); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "User não encontrado", (String) result); + } + + @Test + @Tag("valid") + public void passesParsedIntegerIdToRepository() { + final String id = "00123"; + final String email = "e@example.com"; + final String password = "pwd"; + final String username = "user"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(anyInt())).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); + verify(UserAccRepo).findById(captor.capture()); + assertEquals((int) 123, (int) captor.getValue()); + assertEquals((String) "Usuário Salvo", (String) result); + } + + @Test + @Tag("valid") + public void callsSettersBeforeSave() { + final String id = "20"; + final int parsedId = 20; + final String email = "ordered@example.com"; + final String password = "orderedPwd"; + final String username = "orderedUser"; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + InOrder inOrder = inOrder(user, UserAccRepo); + inOrder.verify(user).setEmail(email); + inOrder.verify(user).setPassword(password); + inOrder.verify(user).setUsername(username); + inOrder.verify(UserAccRepo).save(user); + } + + @Test + @Tag("valid") + public void acceptsWhitespaceOnlyFields() { + final String id = "21"; + final int parsedId = 21; + final String email = " "; + final String password = " "; + final String username = " "; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, email, password, username); + assertEquals((String) "Usuário Salvo", (String) result); + verify(user).setEmail(email); + verify(user).setPassword(password); + verify(user).setUsername(username); + verify(UserAccRepo).save(user); + verifyNoInteractions(UserInfoRepo, Log); + } + + @Test + @Tag("integration") + public void noInteractionWithUserInfoAndLogRepositories() { + // Sub-case (a): success path + { + final String id = "22"; + final int parsedId = 22; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "Usuário Salvo", (String) result); + verifyNoInteractions(UserInfoRepo, Log); + } + reset(UserAccRepo, UserInfoRepo, Log); + // Sub-case (b): not-found path + { + final String id = "23"; + final int parsedId = 23; + when(UserAccRepo.findById(parsedId)).thenReturn(null); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "User não encontrado", (String) result); + verifyNoInteractions(UserInfoRepo, Log); + } + reset(UserAccRepo, UserInfoRepo, Log); + // Sub-case (c): exception path + { + final String id = "24"; + final int parsedId = 24; + when(UserAccRepo.findById(parsedId)).thenThrow(new RuntimeException("err")); + assertThrows(RuntimeException.class, () -> controller.updateUserById(id, "e", "p", "u")); + verifyNoInteractions(UserInfoRepo, Log); + } + } + + @Test + @Tag("boundary") + public void handlesZeroIdDependingOnRepositoryResult() { + // Sub-case (a): findById(0) returns user + { + final String id = "0"; + final int parsedId = 0; + User_Credentials user = mock(User_Credentials.class); + when(UserAccRepo.findById(parsedId)).thenReturn(user); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "Usuário Salvo", (String) result); + verify(UserAccRepo).save(user); + } + reset(UserAccRepo); + // Sub-case (b): findById(0) returns null + { + final String id = "0"; + final int parsedId = 0; + when(UserAccRepo.findById(parsedId)).thenReturn(null); + String result = controller.updateUserById(id, "e", "p", "u"); + assertEquals((String) "User não encontrado", (String) result); + verify(UserAccRepo, never()).save(any()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/medeiros/SPRINGProject/utils/HashMapFunctionsOrdenarHashMapPorValorTest.java b/src/test/java/com/medeiros/SPRINGProject/utils/HashMapFunctionsOrdenarHashMapPorValorTest.java new file mode 100644 index 00000000..618f5b05 --- /dev/null +++ b/src/test/java/com/medeiros/SPRINGProject/utils/HashMapFunctionsOrdenarHashMapPorValorTest.java @@ -0,0 +1,624 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test maven-music-github using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=ordenarHashMapPorValor_29e282f5b3 +ROOST_METHOD_SIG_HASH=ordenarHashMapPorValor_afd4884fe1 + +Scenario 1: Sorts a map with distinct values in strict descending order + +Details: + TestName: sortsDescendingWithDistinctValues + Description: Verifies that the method orders entries by their Integer values in strictly descending order when all values are distinct. + +Execution: + Arrange: Create a HashMap with entries such as {"a"=1, "b"=3, "c"=2}. + Act: Invoke hashMapFunctions.ordenarHashMapPorValor with the prepared map. + Assert: Use assertions to verify the iteration order of the returned map is ["b"=3, "c"=2, "a"=1] and the size equals the input size. + +Validation: + Confirming the exact descending order demonstrates the comparator is applied in reverse (largest to smallest). Ensuring the size matches validates no entries were lost or duplicated. + + +Scenario 2: Returns an empty map when the input is empty + +Details: + TestName: returnsEmptyMapWhenInputIsEmpty + Description: Ensures that providing an empty HashMap returns an empty result without errors. + +Execution: + Arrange: Create an empty HashMap. + Act: Call hashMapFunctions.ordenarHashMapPorValor with the empty map. + Assert: Assert that the returned map is empty and not null. + +Validation: + This validates graceful handling of boundary conditions and ensures no extraneous elements are added. + + +Scenario 3: Returns the same single entry unchanged + +Details: + TestName: returnsSingleEntryUnchanged + Description: Verifies that a single-entry map remains effectively unchanged (same key-value pair present) after sorting. + +Execution: + Arrange: Create a HashMap with one entry, e.g., {"only"=42}. + Act: Call the method with this map. + Assert: Assert the returned map has size 1 and contains the entry "only"=42. + +Validation: + Confirms that sorting preserves data and does not alter content when sorting is trivial. + + +Scenario 4: Preserves relative order for equal values using a LinkedHashMap input + +Details: + TestName: preservesRelativeOrderForEqualValuesUsingLinkedHashMap + Description: Ensures stability for entries with equal values by using a LinkedHashMap (assigned to a HashMap variable) to control input iteration order. + +Execution: + Arrange: Create a LinkedHashMap (referenced as HashMap) with entries inserted in order: "k1"=5, "k2"=5, "k3"=5. + Act: Invoke the method with this map. + Assert: Assert that the returned iteration order is ["k1"=5, "k2"=5, "k3"=5], preserving insertion order among ties. + +Validation: + Sorting with a stable algorithm and a comparator that returns 0 for equal values should preserve original tie order. This confirms sort stability with controlled input ordering. + + +Scenario 5: Maintains stable order within each tie group across multiple groups + +Details: + TestName: maintainsStableOrderWithinEachTieGroup + Description: Verifies that when multiple groups of equal values exist, entries within each group retain their original relative order, and groups themselves are ordered by value descending. + +Execution: + Arrange: Use a LinkedHashMap (referenced as HashMap) with entries: "a"=10, "b"=10, "c"=8, "d"=8, "e"=9 in that insertion order. + Act: Call the method. + Assert: Assert the returned order is ["a"=10, "b"=10, "e"=9, "c"=8, "d"=8], preserving intra-group order and descending between groups. + +Validation: + Confirms both descending value ordering and stability for tie groups, which is critical for predictable output ordering. + + +Scenario 6: Sorts mixed negative, zero, and positive values in descending order + +Details: + TestName: sortsNegativeZeroAndPositiveValuesDescending + Description: Validates correct ordering when the map contains negative numbers, zero, and positive values. + +Execution: + Arrange: Create a HashMap with entries {"neg"=-2, "zero"=0, "pos"=3, "neg2"=-5}. + Act: Invoke the method. + Assert: Assert order is ["pos"=3, "zero"=0, "neg"=-2, "neg2"=-5]. + +Validation: + Ensures compareTo handles sign differences correctly, and the method truly sorts by numerical value descending. + + +Scenario 7: Handles Integer boundary values correctly + +Details: + TestName: handlesIntegerBoundsCorrectly + Description: Ensures correct sorting when values include Integer.MAX_VALUE and Integer.MIN_VALUE. + +Execution: + Arrange: Create a HashMap with entries {"min"=Integer.MIN_VALUE, "mid"=1, "max"=Integer.MAX_VALUE}. + Act: Call the method. + Assert: Assert that the first entry has Integer.MAX_VALUE and the last has Integer.MIN_VALUE. + +Validation: + Confirms that the comparator’s use of compareTo avoids overflow and handles extreme bounds properly. + + +Scenario 8: Accepts a null key and sorts by value normally + +Details: + TestName: includesNullKeyAndSortsByValue + Description: Verifies that a null key is preserved and positioned according to its value. + +Execution: + Arrange: Create a HashMap with entries {null=2, "a"=3, "b"=1}. + Act: Invoke the method. + Assert: Assert order is ["a"=3, null=2, "b"=1], and the null key is present in the result. + +Validation: + Java HashMap allows a null key; since only values are compared, null keys should not cause errors. This test confirms key preservation regardless of null. + + +Scenario 9: Throws NullPointerException when any value is null + +Details: + TestName: throwsNullPointerExceptionWhenAnyValueIsNull + Description: Ensures the method fails fast with a NullPointerException when a map contains a null value, due to compareTo on a null reference. + +Execution: + Arrange: Create a HashMap with entries {"a"=3, "b"=null, "c"=1}. + Act: Call the method and expect an exception. + Assert: Use an assertion (e.g., assertThrows) to verify a NullPointerException is thrown. + +Validation: + The comparator invokes getValue().compareTo(...); a null value triggers a NullPointerException. This test documents and validates the current failure mode. + + +Scenario 10: Throws NullPointerException when the input map reference is null + +Details: + TestName: throwsNullPointerExceptionWhenInputMapIsNull + Description: Verifies behavior when the method receives a null reference for the map parameter. + +Execution: + Arrange: Set the input map reference to null. + Act: Invoke hashMapFunctions.ordenarHashMapPorValor with null. + Assert: Assert that a NullPointerException is thrown. + +Validation: + As the implementation dereferences hashMap.entrySet(), a null input must throw NullPointerException. This confirms expected error handling. + + +Scenario 11: Returns a LinkedHashMap instance that preserves sorted iteration order + +Details: + TestName: returnsLinkedHashMapPreservingSortedIterationOrder + Description: Verifies that the returned map is a LinkedHashMap (preserving insertion/iteration order equal to the sorted order). + +Execution: + Arrange: Prepare a HashMap with several entries to sort (e.g., {"x"=2, "y"=5, "z"=3}). + Act: Call the method. + Assert: Assert that result is an instance of LinkedHashMap and that iterating keys yields order ["y","z","x"]. + +Validation: + Returning a LinkedHashMap ensures deterministic iteration order corresponding to the sorting result, which is essential for predictable downstream behavior. + + +Scenario 12: Does not mutate the original input map + +Details: + TestName: doesNotMutateOriginalMap + Description: Confirms that the source map remains unchanged after the sorting operation. + +Execution: + Arrange: Create a HashMap with known entries and keep a copy of its contents (e.g., capturing its size and that it still maps each key to its original value). + Act: Invoke the sorting method. + Assert: Assert that the original map still contains the same key-value pairs and the same size as before the call. + +Validation: + The method reads entries and builds a new map. This test ensures no side effects on the caller-provided data structure. + + +Scenario 13: Returns a new map instance distinct from the input map + +Details: + TestName: returnsNewMapInstanceNotSameAsInput + Description: Ensures that the method does not return the same instance as the input and instead returns a newly constructed map. + +Execution: + Arrange: Create a HashMap with sample entries. + Act: Call the method and capture the result. + Assert: Use assertions to verify that the returned map reference is not the same as the input map reference. + +Validation: + Guarantees that callers receive an independent map, preventing unexpected aliasing or side effects. + + +Scenario 14: Returned map remains unchanged if the input map is mutated after sorting + +Details: + TestName: returnedMapUnaffectedByFurtherInputMutations + Description: Confirms that subsequent changes to the original input map do not affect the already returned sorted map. + +Execution: + Arrange: Create a HashMap with entries and call the method to obtain the sorted map. Then mutate the input map (e.g., add, remove, or update entries). + Act: Do not re-invoke the method; simply inspect the returned map. + Assert: Assert that the returned map’s contents and iteration order remain as initially produced. + +Validation: + Since the method constructs a new LinkedHashMap from a snapshot of entries, the returned map should be independent of later changes to the input. + + +Scenario 15: Sorts a large dataset while maintaining non-increasing order of values + +Details: + TestName: sortsLargeDatasetInNonIncreasingOrder + Description: Validates that sorting works and scales for larger inputs by ensuring the resulting values are monotonically non-increasing. + +Execution: + Arrange: Create a HashMap with a large number of entries (e.g., 1000 keys) with varied random Integer values. + Act: Invoke the method. + Assert: Iterate through the result and assert that each subsequent value is less than or equal to the previous value; also assert sizes match. + +Validation: + Confirms algorithmic correctness and robustness under heavier loads, ensuring consistent ordering for all entries. + + +Scenario 16: Accepts a LinkedHashMap instance as input via its HashMap type + +Details: + TestName: acceptsLinkedHashMapInstanceAsInput + Description: Ensures compatibility when the input variable is typed as HashMap but instantiated as LinkedHashMap. + +Execution: + Arrange: Instantiate a LinkedHashMap (assigned to a HashMap reference) with entries in a known insertion order, including ties. + Act: Call the method. + Assert: Assert that the returned ordering reflects descending values and stable ordering for ties. + +Validation: + As LinkedHashMap extends HashMap, this verifies the method handles subclass instances correctly while leveraging deterministic insertion order for stable tie validation. + + +Scenario 17: Ties in a plain HashMap input do not require a specific relative order among equals + +Details: + TestName: tiesInHashMapInputDoNotRequireSpecificOrderAmongEquals + Description: Ensures correctness by verifying value-based descending order without assuming any specific relative order among keys that have equal values when the input is a standard HashMap. + +Execution: + Arrange: Create a HashMap with multiple keys sharing the same values (e.g., two keys with value 5, three keys with value 2) and additional distinct values. + Act: Invoke the method. + Assert: Assert that the sequence of values is non-increasing and that, for each tie value, all expected keys are present in a contiguous block, without asserting their internal order. + +Validation: + Because HashMap iteration order is unspecified, tie order cannot be assumed. This test focuses on the guaranteed contract: correct descending value ordering and presence of all entries. + + +Scenario 18: Preserves key-value associations after sorting + +Details: + TestName: preservesKeyValueAssociationsAfterSorting + Description: Ensures that keys remain paired with their original values and no cross-assignment occurs during sorting. + +Execution: + Arrange: Create a HashMap with distinct key-value pairs (e.g., {"k1"=10, "k2"=7, "k3"=9}). + Act: Call the method. + Assert: Assert that each key in the result still maps to the same Integer value as in the input. + +Validation: + Confirms that sorting reorders entries by value but does not alter key-value bindings, preserving data integrity. + +*/ + +// ********RoostGPT******** + +package com.medeiros.SPRINGProject.utils; + +import java.util.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.*; + +public class HashMapFunctionsOrdenarHashMapPorValorTest { + + @Test + @Tag("valid") + @DisplayName("Scenario 1: Sorts a map with distinct values in strict descending order") + public void testSortsDescendingWithDistinctValues() { + HashMap input = new HashMap<>(); + input.put("a", 1); + input.put("b", 3); + input.put("c", 2); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + } + List expectedKeysOrder = Arrays.asList("b", "c", "a"); + assertEquals((int) expectedKeysOrder.size(), (int) keysInOrder.size(), "Size mismatch after sorting"); + assertIterableEquals(expectedKeysOrder, keysInOrder, "Keys order should be [b, c, a]"); + assertEquals((int) input.size(), (int) result.size(), "Result size should equal input size"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 2: Returns an empty map when the input is empty") + public void testReturnsEmptyMapWhenInputIsEmpty() { + HashMap input = new HashMap<>(); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + assertNotNull(result, "Result should not be null"); + assertTrue(result.isEmpty(), "Result should be empty when input is empty"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 3: Returns the same single entry unchanged") + public void testReturnsSingleEntryUnchanged() { + HashMap input = new HashMap<>(); + input.put("only", 42); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + assertEquals((int) 1, (int) result.size(), "Size should be 1"); + assertTrue(result.containsKey("only"), "Result should contain key 'only'"); + assertEquals((Integer) 42, (Integer) result.get("only"), "Value for 'only' should be 42"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 4: Preserves relative order for equal values using a LinkedHashMap input") + public void testPreservesRelativeOrderForEqualValuesUsingLinkedHashMap() { + HashMap input = new LinkedHashMap<>(); + input.put("k1", 5); + input.put("k2", 5); + input.put("k3", 5); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + assertEquals((Integer) 5, (Integer) e.getValue(), "All values should be 5"); + } + List expected = Arrays.asList("k1", "k2", "k3"); + assertIterableEquals(expected, keysInOrder, "Tie order should preserve insertion order"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 5: Maintains stable order within each tie group across multiple groups") + public void testMaintainsStableOrderWithinEachTieGroup() { + HashMap input = new LinkedHashMap<>(); + input.put("a", 10); + input.put("b", 10); + input.put("c", 8); + input.put("d", 8); + input.put("e", 9); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + } + List expected = Arrays.asList("a", "b", "e", "c", "d"); + assertIterableEquals(expected, keysInOrder, "Order should be [a, b, e, c, d]"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 6: Sorts mixed negative, zero, and positive values in descending order") + public void testSortsNegativeZeroAndPositiveValuesDescending() { + HashMap input = new HashMap<>(); + input.put("neg", -2); + input.put("zero", 0); + input.put("pos", 3); + input.put("neg2", -5); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + } + List expected = Arrays.asList("pos", "zero", "neg", "neg2"); + assertIterableEquals(expected, keysInOrder, + "Expected descending order with negatives and zero handled correctly"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 7: Handles Integer boundary values correctly") + public void testHandlesIntegerBoundsCorrectly() { + HashMap input = new HashMap<>(); + input.put("min", Integer.MIN_VALUE); + input.put("mid", 1); + input.put("max", Integer.MAX_VALUE); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List values = new ArrayList<>(result.values()); + assertEquals((Integer) Integer.MAX_VALUE, (Integer) values.get(0), "First value should be Integer.MAX_VALUE"); + assertEquals((Integer) Integer.MIN_VALUE, (Integer) values.get(values.size() - 1), + "Last value should be Integer.MIN_VALUE"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 8: Accepts a null key and sorts by value normally") + public void testIncludesNullKeyAndSortsByValue() { + HashMap input = new HashMap<>(); + input.put(null, 2); + input.put("a", 3); + input.put("b", 1); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + } + List expected = Arrays.asList("a", null, "b"); + assertIterableEquals(expected, keysInOrder, "Order should place null key according to its value"); + assertTrue(result.containsKey(null), "Result should contain null key"); + assertEquals((Integer) 2, (Integer) result.get(null), "Null key should map to value 2"); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 9: Throws NullPointerException when any value is null") + public void testThrowsNullPointerExceptionWhenAnyValueIsNull() { + HashMap input = new HashMap<>(); + input.put("a", 3); + input.put("b", null); + input.put("c", 1); + assertThrows(NullPointerException.class, () -> { + hashMapFunctions.ordenarHashMapPorValor(input); + }, "Expected NullPointerException when a value is null"); + } + + @Test + @Tag("invalid") + @DisplayName("Scenario 10: Throws NullPointerException when the input map reference is null") + public void testThrowsNullPointerExceptionWhenInputMapIsNull() { + HashMap input = null; + assertThrows(NullPointerException.class, () -> { + hashMapFunctions.ordenarHashMapPorValor(input); + }, "Expected NullPointerException when input is null"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 11: Returns a LinkedHashMap instance that preserves sorted iteration order") + public void testReturnsLinkedHashMapPreservingSortedIterationOrder() { + HashMap input = new HashMap<>(); + input.put("x", 2); + input.put("y", 5); + input.put("z", 3); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + assertTrue(result instanceof LinkedHashMap, "Resulting map should be an instance of LinkedHashMap"); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + } + List expected = Arrays.asList("y", "z", "x"); + assertIterableEquals(expected, keysInOrder, "Keys should be ordered as [y, z, x]"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 12: Does not mutate the original input map") + public void testDoesNotMutateOriginalMap() { + HashMap input = new HashMap<>(); + input.put("p", 4); + input.put("q", 1); + input.put("r", 3); + HashMap snapshot = new HashMap<>(input); + hashMapFunctions.ordenarHashMapPorValor(input); + assertEquals((int) snapshot.size(), (int) input.size(), "Input size should remain unchanged"); + for (Map.Entry e : snapshot.entrySet()) { + assertTrue(input.containsKey(e.getKey()), "Input should still contain key: " + e.getKey()); + assertEquals((Integer) e.getValue(), (Integer) input.get(e.getKey()), + "Input value should remain unchanged for key: " + e.getKey()); + } + } + + @Test + @Tag("valid") + @DisplayName("Scenario 13: Returns a new map instance distinct from the input map") + public void testReturnsNewMapInstanceNotSameAsInput() { + HashMap input = new HashMap<>(); + input.put("s", 2); + input.put("t", 3); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + assertNotSame(input, result, "Returned map should not be the same instance as input"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 14: Returned map remains unchanged if the input map is mutated after sorting") + public void testReturnedMapUnaffectedByFurtherInputMutations() { + HashMap input = new HashMap<>(); + input.put("u", 7); + input.put("v", 1); + input.put("w", 5); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List> resultBefore = new ArrayList<>(result.entrySet()); + // Mutate input after sorting + input.put("newKey", 100); // TODO: Adjust this value if needed for different + // mutation scenarios + input.remove("v"); + input.put("u", 0); + List> resultAfter = new ArrayList<>(result.entrySet()); + assertIterableEquals(resultBefore, resultAfter, "Returned map should remain unchanged after input mutations"); + } + + @Test + @Tag("boundary") + @DisplayName("Scenario 15: Sorts a large dataset while maintaining non-increasing order of values") + public void testSortsLargeDatasetInNonIncreasingOrder() { + HashMap input = new HashMap<>(); + Random rnd = new Random(12345); + int size = 1000; // TODO: Change dataset size if needed for performance testing + for (int i = 0; i < size; i++) { + input.put("k" + i, rnd.nextInt(10000)); + } + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + assertEquals((int) input.size(), (int) result.size(), "Result size should equal input size for large dataset"); + Integer prev = null; + for (Integer val : result.values()) { + if (prev != null) { + assertTrue(prev >= val, "Values should be non-increasing throughout the iteration"); + } + prev = val; + } + } + + @Test + @Tag("valid") + @DisplayName("Scenario 16: Accepts a LinkedHashMap instance as input via its HashMap type") + public void testAcceptsLinkedHashMapInstanceAsInput() { + HashMap input = new LinkedHashMap<>(); + input.put("a", 2); + input.put("b", 3); + input.put("c", 3); + input.put("d", 1); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + List keysInOrder = new ArrayList<>(); + for (Map.Entry e : result.entrySet()) { + keysInOrder.add(e.getKey()); + } + List expected = Arrays.asList("b", "c", "a", "d"); + assertIterableEquals(expected, keysInOrder, "Order should reflect descending values and stable tie ordering"); + } + + @Test + @Tag("valid") + @DisplayName("Scenario 17: Ties in a plain HashMap input do not require a specific relative order among equals") + public void testTiesInHashMapInputDoNotRequireSpecificOrderAmongEquals() { + HashMap input = new HashMap<>(); + input.put("t1", 5); + input.put("t2", 5); + input.put("u1", 2); + input.put("u2", 2); + input.put("u3", 2); + input.put("v", 7); + input.put("w", 1); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + // Assert non-increasing order by values + Integer prev = null; + for (Integer val : result.values()) { + if (prev != null) { + assertTrue(prev >= val, "Values should be non-increasing"); + } + prev = val; + } + // Verify that tie groups are contiguous and contain all expected keys + Map> expectedGroups = new HashMap<>(); + expectedGroups.put(7, new HashSet<>(Arrays.asList("v"))); + expectedGroups.put(5, new HashSet<>(Arrays.asList("t1", "t2"))); + expectedGroups.put(2, new HashSet<>(Arrays.asList("u1", "u2", "u3"))); + expectedGroups.put(1, new HashSet<>(Arrays.asList("w"))); + // Build actual groups based on contiguous segments of equal values + List> entries = new ArrayList<>(result.entrySet()); + int i = 0; + List encounteredValuesOrder = new ArrayList<>(); + while (i < entries.size()) { + int val = entries.get(i).getValue(); + encounteredValuesOrder.add(val); + Set keysForVal = new HashSet<>(); + int j = i; + while (j < entries.size() && Objects.equals(entries.get(j).getValue(), val)) { + keysForVal.add(entries.get(j).getKey()); + j++; + } + assertTrue(expectedGroups.containsKey(val), "Unexpected value group encountered: " + val); + assertEquals((int) expectedGroups.get(val).size(), (int) keysForVal.size(), + "Tie group size mismatch for value: " + val); + assertEquals(expectedGroups.get(val), keysForVal, "Tie group keys mismatch for value: " + val); + i = j; + } + // Verify that value groups are ordered descending + Integer prior = null; + for (Integer val : encounteredValuesOrder) { + if (prior != null) { + assertTrue(prior > val, "Distinct value groups should be strictly descending"); + } + prior = val; + } + } + + @Test + @Tag("valid") + @DisplayName("Scenario 18: Preserves key-value associations after sorting") + public void testPreservesKeyValueAssociationsAfterSorting() { + HashMap input = new HashMap<>(); + input.put("k1", 10); + input.put("k2", 7); + input.put("k3", 9); + HashMap result = hashMapFunctions.ordenarHashMapPorValor(input); + for (Map.Entry e : input.entrySet()) { + assertTrue(result.containsKey(e.getKey()), "Result should contain key: " + e.getKey()); + assertEquals((Integer) e.getValue(), (Integer) result.get(e.getKey()), + "Value should remain associated with key: " + e.getKey()); + } + } + +} \ No newline at end of file