diff --git a/src/main/java/teammates/ui/webapi/GetTimeZonesAction.java b/src/main/java/teammates/ui/webapi/GetTimeZonesAction.java index c1f9bbc4385..34c0c7ee1dc 100644 --- a/src/main/java/teammates/ui/webapi/GetTimeZonesAction.java +++ b/src/main/java/teammates/ui/webapi/GetTimeZonesAction.java @@ -11,7 +11,7 @@ /** * Action: get supported time zones. */ -class GetTimeZonesAction extends Action { +public class GetTimeZonesAction extends Action { @Override AuthType getMinAuthLevel() { diff --git a/src/main/java/teammates/ui/webapi/GetUsageStatisticsAction.java b/src/main/java/teammates/ui/webapi/GetUsageStatisticsAction.java index 8db63f5019b..30032ddb192 100644 --- a/src/main/java/teammates/ui/webapi/GetUsageStatisticsAction.java +++ b/src/main/java/teammates/ui/webapi/GetUsageStatisticsAction.java @@ -11,7 +11,7 @@ /** * Gets usage statistics for a specified time period. */ -class GetUsageStatisticsAction extends Action { +public class GetUsageStatisticsAction extends Action { private static final Duration MAX_SEARCH_WINDOW = Duration.ofDays(184L); // covering six whole months diff --git a/src/test/java/teammates/sqlui/webapi/CreateNotificationActionTest.java b/src/test/java/teammates/sqlui/webapi/CreateNotificationActionTest.java new file mode 100644 index 00000000000..a30a5361216 --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/CreateNotificationActionTest.java @@ -0,0 +1,192 @@ +package teammates.sqlui.webapi; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.when; + +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.NotificationStyle; +import teammates.common.datatransfer.NotificationTargetUser; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.storage.sqlentity.Notification; +import teammates.ui.output.NotificationData; +import teammates.ui.request.InvalidHttpRequestBodyException; +import teammates.ui.request.NotificationCreateRequest; +import teammates.ui.webapi.CreateNotificationAction; + +/** + * SUT: {@link CreateNotificationAction}. + */ +public class CreateNotificationActionTest extends BaseActionTest { + private static final String GOOGLE_ID = "user-googleId"; + private static final String INVALID_TITLE = ""; + NotificationCreateRequest testReq; + private Notification testNotification; + + @Override + String getActionUri() { + return Const.ResourceURIs.NOTIFICATION; + } + + @Override + String getRequestMethod() { + return POST; + } + + @BeforeMethod + void setUp() { + loginAsAdmin(); + testNotification = getTypicalNotificationWithId(); + } + + @Test + void testExecute_addNotification_success() throws Exception { + long startTime = testNotification.getStartTime().toEpochMilli(); + long endTime = testNotification.getEndTime().toEpochMilli(); + NotificationStyle style = testNotification.getStyle(); + NotificationTargetUser targetUser = testNotification.getTargetUser(); + String title = testNotification.getTitle(); + String message = testNotification.getMessage(); + + when(mockLogic.createNotification(isA(Notification.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + NotificationCreateRequest req = getTypicalCreateRequest(); + CreateNotificationAction action = getAction(req); + NotificationData res = (NotificationData) action.execute().getOutput(); + + when(mockLogic.getNotification(UUID.fromString(res.getNotificationId()))).thenReturn(testNotification); + + Notification createdNotification = mockLogic.getNotification(UUID.fromString(res.getNotificationId())); + + // check that notification returned has same properties as notification created + assertEquals(createdNotification.getStartTime().toEpochMilli(), res.getStartTimestamp()); + assertEquals(createdNotification.getEndTime().toEpochMilli(), res.getEndTimestamp()); + assertEquals(createdNotification.getStyle(), res.getStyle()); + assertEquals(createdNotification.getTargetUser(), res.getTargetUser()); + assertEquals(createdNotification.getTitle(), res.getTitle()); + assertEquals(createdNotification.getMessage(), res.getMessage()); + + // check DB correctly processed request + assertEquals(startTime, createdNotification.getStartTime().toEpochMilli()); + assertEquals(endTime, createdNotification.getEndTime().toEpochMilli()); + assertEquals(style, createdNotification.getStyle()); + assertEquals(targetUser, createdNotification.getTargetUser()); + assertEquals(title, createdNotification.getTitle()); + assertEquals(message, createdNotification.getMessage()); + } + + @Test + void testSpecificAccessControl_admin_canAccess() { + verifyCanAccess(); + } + + @Test + void testSpecificAccessControl_instructor_cannotAccess() { + logoutUser(); + loginAsInstructor(GOOGLE_ID); + verifyCannotAccess(); + } + + @Test + void testSpecificAccessControl_student_cannotAccess() { + logoutUser(); + loginAsStudent(GOOGLE_ID); + verifyCannotAccess(); + } + + @Test + void testSpecificAccessControl_loggedOut_cannotAccess() { + logoutUser(); + verifyCannotAccess(); + } + + @Test + void testExecute_invalidStyle_throwsInvalidHttpParameterException() { + testReq = getTypicalCreateRequest(); + testReq.setStyle(null); + + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("Notification style cannot be null", ex.getMessage()); + } + + @Test + void testExecute_invalidTargetUser_throwsInvalidHttpParameterException() { + testReq = getTypicalCreateRequest(); + testReq.setTargetUser(null); + + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("Notification target user cannot be null", ex.getMessage()); + } + + @Test + void testExecute_invalidTitle_throwsInvalidHttpParameterException() { + testReq = getTypicalCreateRequest(); + testReq.setTitle(null); + + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("Notification title cannot be null", ex.getMessage()); + } + + @Test + void testExecute_invalidMessage_throwsInvalidHttpParameterException() { + testReq = getTypicalCreateRequest(); + testReq.setMessage(null); + + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("Notification message cannot be null", ex.getMessage()); + } + + @Test + void testExecute_negativeStartTimestamp_throwsInvalidHttpParameterException() { + testReq = getTypicalCreateRequest(); + testReq.setStartTimestamp(-1); + + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("Start timestamp should be greater than zero", ex.getMessage()); + } + + @Test + void testExecute_negativeEndTimestamp_throwsInvalidHttpParameterException() { + testReq = getTypicalCreateRequest(); + testReq.setEndTimestamp(-1); + + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("End timestamp should be greater than zero", ex.getMessage()); + } + + @Test + void testExecute_invalidParameter_throwsInvalidHttpParameterException() throws Exception { + testReq = getTypicalCreateRequest(); + testReq.setTitle(INVALID_TITLE); + + when(mockLogic.createNotification(any())).thenThrow(new InvalidParametersException("Invalid title")); + InvalidHttpRequestBodyException ex = verifyHttpRequestBodyFailure(testReq); + + assertEquals("Invalid title", ex.getMessage()); + } + + private NotificationCreateRequest getTypicalCreateRequest() { + NotificationCreateRequest req = new NotificationCreateRequest(); + + req.setStartTimestamp(testNotification.getStartTime().toEpochMilli()); + req.setEndTimestamp(testNotification.getEndTime().toEpochMilli()); + req.setStyle(testNotification.getStyle()); + req.setTargetUser(testNotification.getTargetUser()); + req.setTitle(testNotification.getTitle()); + req.setMessage(testNotification.getMessage()); + + return req; + } + +} diff --git a/src/test/java/teammates/sqlui/webapi/DeleteFeedbackQuestionActionTest.java b/src/test/java/teammates/sqlui/webapi/DeleteFeedbackQuestionActionTest.java index 8af644c8e7c..02cc1310dff 100644 --- a/src/test/java/teammates/sqlui/webapi/DeleteFeedbackQuestionActionTest.java +++ b/src/test/java/teammates/sqlui/webapi/DeleteFeedbackQuestionActionTest.java @@ -5,6 +5,7 @@ import java.util.UUID; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import teammates.common.datatransfer.InstructorPrivileges; @@ -21,11 +22,10 @@ */ public class DeleteFeedbackQuestionActionTest extends BaseActionTest { - private final Instructor typicalInstructor = getTypicalInstructor(); - private final Course typicalCourse = typicalInstructor.getCourse(); - private final FeedbackSession typicalFeedbackSession = getTypicalFeedbackSessionForCourse(typicalCourse); - private final FeedbackQuestion typicalFeedbackQuestion = - getTypicalFeedbackQuestionForSession(typicalFeedbackSession); + private Instructor typicalInstructor; + private Course typicalCourse; + private FeedbackSession typicalFeedbackSession; + private FeedbackQuestion typicalFeedbackQuestion; @Override protected String getActionUri() { @@ -37,6 +37,14 @@ protected String getRequestMethod() { return DELETE; } + @BeforeMethod + void setUp() { + typicalInstructor = getTypicalInstructor(); + typicalCourse = typicalInstructor.getCourse(); + typicalFeedbackSession = getTypicalFeedbackSessionForCourse(typicalCourse); + typicalFeedbackQuestion = getTypicalFeedbackQuestionForSession(typicalFeedbackSession); + } + @Test void testExecute_feedbackQuestionExists_success() { when(mockLogic.getFeedbackQuestion(typicalFeedbackQuestion.getId())).thenReturn(typicalFeedbackQuestion); diff --git a/src/test/java/teammates/sqlui/webapi/GetNotificationActionTest.java b/src/test/java/teammates/sqlui/webapi/GetNotificationActionTest.java new file mode 100644 index 00000000000..3421b81202d --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/GetNotificationActionTest.java @@ -0,0 +1,84 @@ +package teammates.sqlui.webapi; + +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import java.util.UUID; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.util.Const; +import teammates.storage.sqlentity.Notification; +import teammates.ui.output.NotificationData; +import teammates.ui.webapi.EntityNotFoundException; +import teammates.ui.webapi.GetNotificationAction; +import teammates.ui.webapi.InvalidHttpParameterException; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link GetNotificationAction}. + */ +public class GetNotificationActionTest extends BaseActionTest { + + @Override + String getActionUri() { + return Const.ResourceURIs.NOTIFICATION; + } + + @Override + String getRequestMethod() { + return GET; + } + + @BeforeMethod + public void baseClassSetup() { + loginAsAdmin(); + } + + @Test + protected void testExecute_withValidNotificationId_shouldReturnData() { + Notification testNotification = getTypicalNotificationWithId(); + NotificationData expected = new NotificationData(testNotification); + + String[] requestParams = new String[] { + Const.ParamsNames.NOTIFICATION_ID, String.valueOf(testNotification.getId()), + }; + + when(mockLogic.getNotification(testNotification.getId())).thenReturn(testNotification); + + GetNotificationAction action = getAction(requestParams); + JsonResult jsonResult = getJsonResult(action); + + NotificationData output = (NotificationData) jsonResult.getOutput(); + verifyNotificationEquals(expected, output); + + reset(mockLogic); + } + + @Test + protected void testExecute_nonExistentNotification_shouldThrowError() { + GetNotificationAction action = getAction(Const.ParamsNames.NOTIFICATION_ID, UUID.randomUUID().toString()); + EntityNotFoundException enfe = assertThrows(EntityNotFoundException.class, action::execute); + + assertEquals("Notification does not exist.", enfe.getMessage()); + } + + @Test + protected void testExecute_notificationIdIsNull_shouldThrowError() { + GetNotificationAction action = getAction(Const.ParamsNames.NOTIFICATION_ID, null, new String[] {}); + InvalidHttpParameterException ihpe = assertThrows(InvalidHttpParameterException.class, action::execute); + + assertEquals("The [notificationid] HTTP parameter is null.", ihpe.getMessage()); + } + + private void verifyNotificationEquals(NotificationData expected, NotificationData actual) { + assertEquals(expected.getNotificationId(), actual.getNotificationId()); + assertEquals(expected.getStyle(), actual.getStyle()); + assertEquals(expected.getTargetUser(), actual.getTargetUser()); + assertEquals(expected.getTitle(), actual.getTitle()); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getStartTimestamp(), actual.getStartTimestamp()); + assertEquals(expected.getEndTimestamp(), actual.getEndTimestamp()); + } +} diff --git a/src/test/java/teammates/sqlui/webapi/GetTimeZonesActionTest.java b/src/test/java/teammates/sqlui/webapi/GetTimeZonesActionTest.java new file mode 100644 index 00000000000..5ef8296b853 --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/GetTimeZonesActionTest.java @@ -0,0 +1,83 @@ +package teammates.sqlui.webapi; + +import org.testng.annotations.Test; + +import teammates.common.util.Const; +import teammates.ui.output.TimeZonesData; +import teammates.ui.webapi.GetTimeZonesAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link GetTimeZonesAction}. + */ +public class GetTimeZonesActionTest extends BaseActionTest { + + @Override + protected String getActionUri() { + return Const.ResourceURIs.TIMEZONE; + } + + @Override + protected String getRequestMethod() { + return GET; + } + + @Test + void testAccessControl_admin_canAccess() { + loginAsAdmin(); + verifyCanAccess(); + } + + @Test + void testAccessControl_maintainers_canAccess() { + loginAsMaintainer(); + verifyCanAccess(); + } + + @Test + void testAccessControl_instructor_cannotAccess() { + loginAsInstructor(Const.ParamsNames.INSTRUCTOR_ID); + verifyCannotAccess(); + } + + @Test + void testAccessControl_student_cannotAccess() { + loginAsStudent(Const.ParamsNames.STUDENT_ID); + verifyCannotAccess(); + } + + @Test + void testAccessControl_loggedOut_cannotAccess() { + logoutUser(); + verifyCannotAccess(); + } + + @Test + void testAccessControl_unregistered_cannotAccess() { + loginAsUnregistered(Const.ParamsNames.USER_ID); + verifyCannotAccess(); + } + + @Test + protected void testExecute_normalCase_shouldSucceed() { + GetTimeZonesAction a = getAction(); + JsonResult r = getJsonResult(a); + + TimeZonesData output = (TimeZonesData) r.getOutput(); + + // This test does not check the timezone database used is the latest + // Only check that the version number is returned, and some sample values for timezone offset + assertNotNull(output.getVersion()); + + // There is a quirk in the ETC/GMT time zones due to the tzdb using POSIX-style signs in the zone names and the + // output abbreviations. POSIX has positive signs west of Greenwich, while we are used to positive signs east + // of Greenwich in practice. For example, TZ='Etc/GMT+8' uses the abbreviation "GMT+8" and corresponds to 8 + // hours behind UTC (i.e. west of Greenwich) even though many people would expect it to mean 8 hours ahead of + // UTC (i.e. east of Greenwich; like Singapore or China). + // (adapted from tzdb table comments) + assertEquals(8 * 60 * 60, output.getOffsets().get("Etc/GMT-8").intValue()); + assertEquals(-5 * 60 * 60, output.getOffsets().get("Etc/GMT+5").intValue()); + assertEquals(11 * 60 * 60, output.getOffsets().get("Etc/GMT-11").intValue()); + assertEquals(0, output.getOffsets().get("Etc/GMT+0").intValue()); + } +} diff --git a/src/test/java/teammates/sqlui/webapi/GetUsageStatisticsActionTest.java b/src/test/java/teammates/sqlui/webapi/GetUsageStatisticsActionTest.java new file mode 100644 index 00000000000..71f83c9ba4b --- /dev/null +++ b/src/test/java/teammates/sqlui/webapi/GetUsageStatisticsActionTest.java @@ -0,0 +1,135 @@ +package teammates.sqlui.webapi; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.util.Const; +import teammates.ui.webapi.GetUsageStatisticsAction; + +/** + * SUT: {@link GetUsageStatisticsAction}. + */ +public class GetUsageStatisticsActionTest extends BaseActionTest { + private static final long START_TIME_FOR_FAIL_CASES = Instant.now().minusSeconds(60).toEpochMilli(); + private static final long END_TIME_FOR_FAIL_CASES = START_TIME_FOR_FAIL_CASES - 1000; + + @Override + protected String getActionUri() { + return Const.ResourceURIs.USAGE_STATISTICS; + } + + @Override + protected String getRequestMethod() { + return GET; + } + + @Test + void testAccessControl_admin_canAccess() { + verifyCanAccess(); + } + + @Test + void testAccessControl_maintainers_canAccess() { + logoutUser(); + loginAsMaintainer(); + verifyCanAccess(); + } + + @Test + void testAccessControl_instructor_cannotAccess() { + logoutUser(); + loginAsInstructor(Const.ParamsNames.INSTRUCTOR_ID); + verifyCannotAccess(); + } + + @Test + void testAccessControl_student_cannotAccess() { + logoutUser(); + loginAsStudent(Const.ParamsNames.STUDENT_ID); + verifyCannotAccess(); + } + + @Test + void testAccessControl_loggedOut_cannotAccess() { + logoutUser(); + verifyCannotAccess(); + } + + @Test + void testAccessControl_unregistered_cannotAccess() { + logoutUser(); + loginAsUnregistered(Const.ParamsNames.USER_ID); + verifyCannotAccess(); + } + + @BeforeMethod + public void setUp() { + loginAsAdmin(); + } + + @Test + void testExecute_success() { + GetUsageStatisticsAction action = getAction( + Const.ParamsNames.QUERY_LOGS_STARTTIME, String.valueOf(START_TIME_FOR_FAIL_CASES), + Const.ParamsNames.QUERY_LOGS_ENDTIME, String.valueOf(START_TIME_FOR_FAIL_CASES + 1000) + ); + // For now, we stop at simply checking that the request is successful, + // as we do not have means to reliably create test usage attributes data yet. + getJsonResult(action); + } + + @Test + void testExecute_endTimeBeforeStart_shouldFail() { + String[] paramsInvalid = { + Const.ParamsNames.QUERY_LOGS_STARTTIME, String.valueOf(START_TIME_FOR_FAIL_CASES), + Const.ParamsNames.QUERY_LOGS_ENDTIME, String.valueOf(END_TIME_FOR_FAIL_CASES), + }; + verifyHttpParameterFailure(paramsInvalid); + } + + @Test + void testExecute_invalidSearchStartTime_shouldFail() { + String[] paramsInvalid = { + Const.ParamsNames.QUERY_LOGS_STARTTIME, "abc", + Const.ParamsNames.QUERY_LOGS_ENDTIME, String.valueOf(END_TIME_FOR_FAIL_CASES), + }; + verifyHttpParameterFailure(paramsInvalid); + } + + @Test + void testExecute_invalidSearchEndTime_shouldFail() { + String[] paramsInvalid = { + Const.ParamsNames.QUERY_LOGS_STARTTIME, String.valueOf(START_TIME_FOR_FAIL_CASES), + Const.ParamsNames.QUERY_LOGS_ENDTIME, " ", + }; + verifyHttpParameterFailure(paramsInvalid); + } + + @Test + void testExecute_searchWindowTooLong_shouldFail() { + long daysExceedingSearchWindow = 200L; + long startTime = Instant.now().minus(daysExceedingSearchWindow, ChronoUnit.DAYS).toEpochMilli(); + long millisExceedingSearchWindow = Duration.ofDays(daysExceedingSearchWindow).toMillis(); + + String[] paramsInvalid = { + Const.ParamsNames.QUERY_LOGS_STARTTIME, String.valueOf(startTime), + Const.ParamsNames.QUERY_LOGS_ENDTIME, String.valueOf(startTime + millisExceedingSearchWindow), + }; + verifyHttpParameterFailure(paramsInvalid); + } + + @Test + void testExecute_endTimeAfterCurrentTime_shouldFail() { + long millisExceedingNow = Instant.now().plusMillis(1000).toEpochMilli(); + String[] paramsInvalid = { + Const.ParamsNames.QUERY_LOGS_STARTTIME, String.valueOf(START_TIME_FOR_FAIL_CASES), + Const.ParamsNames.QUERY_LOGS_ENDTIME, String.valueOf(millisExceedingNow), + }; + verifyHttpParameterFailure(paramsInvalid); + } + +}