diff --git a/solr/solr.sh b/solr/solr.sh index 401022a87d7..f29234bccd6 100755 --- a/solr/solr.sh +++ b/solr/solr.sh @@ -17,3 +17,5 @@ bin/solr create -c accountrequests -s 2 -rf 2 bin/solr config -c accountrequests -p 8983 -action set-user-property -property update.autoCreateFields -value false curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "email", "type": "string"}}' localhost:8983/solr/accountrequests/schema curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "institute", "type": "string"}}' localhost:8983/solr/accountrequests/schema +curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "comments", "type": "string"}}' localhost:8983/solr/accountrequests/schema +curl -X POST -H 'Content-type: application/json' --data-binary '{"add-field": {"name": "status", "type": "string"}}' localhost:8983/solr/accountrequests/schema diff --git a/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java b/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java index bef8085455b..21abcf9e462 100644 --- a/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java +++ b/src/client/java/teammates/client/scripts/sql/DataMigrationForAccountRequestSql.java @@ -3,6 +3,7 @@ // CHECKSTYLE.OFF:ImportOrder import com.googlecode.objectify.cmd.Query; +import teammates.common.datatransfer.AccountRequestStatus; import jakarta.persistence.criteria.CriteriaDelete; import teammates.common.util.HibernateUtil; @@ -57,7 +58,9 @@ protected void migrateEntity(teammates.storage.entity.AccountRequest oldEntity) AccountRequest newEntity = new AccountRequest( oldEntity.getEmail(), oldEntity.getName(), - oldEntity.getInstitute()); + oldEntity.getInstitute(), + AccountRequestStatus.APPROVED, + null); // set registration key to the old value if exists if (oldEntity.getRegistrationKey() != null) { diff --git a/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java b/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java index 909c8ac649e..c26147b4016 100644 --- a/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java +++ b/src/client/java/teammates/client/scripts/sql/VerifyDataMigrationConnection.java @@ -7,6 +7,7 @@ import teammates.client.connector.DatastoreClient; import teammates.client.util.ClientProperties; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.util.HibernateUtil; import teammates.storage.entity.UsageStatistics; import teammates.storage.sqlentity.Notification; @@ -43,7 +44,9 @@ protected void verifySqlConnection() { teammates.storage.sqlentity.AccountRequest newEntity = new teammates.storage.sqlentity.AccountRequest( "dummy-teammates-account-request-email@gmail.com", "dummy-teammates-account-request", - "dummy-teammates-institute"); + "dummy-teammates-institute", + AccountRequestStatus.PENDING, + "dummy-comments"); HibernateUtil.beginTransaction(); HibernateUtil.persist(newEntity); HibernateUtil.commitTransaction(); diff --git a/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java b/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java index 69acaad7975..8b899504833 100644 --- a/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java +++ b/src/client/java/teammates/client/scripts/testdataconversion/DataStoreToSqlConverter.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.UUID; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.InstructorPermissionRole; import teammates.common.datatransfer.attributes.AccountAttributes; import teammates.common.datatransfer.attributes.AccountRequestAttributes; @@ -128,7 +129,11 @@ protected Account convert(AccountAttributes accAttr) { protected AccountRequest convert(AccountRequestAttributes accReqAttr) { AccountRequest sqlAccountRequest = new AccountRequest(accReqAttr.getEmail(), accReqAttr.getName(), - accReqAttr.getInstitute()); + accReqAttr.getInstitute(), AccountRequestStatus.APPROVED, null); + + if (accReqAttr.getRegisteredAt() != null) { + sqlAccountRequest.setStatus(AccountRequestStatus.REGISTERED); + } sqlAccountRequest.setCreatedAt(accReqAttr.getCreatedAt()); sqlAccountRequest.setRegisteredAt(accReqAttr.getRegisteredAt()); diff --git a/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java index 6cfb6b478e0..014913e639c 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminHomePageE2ETest.java @@ -5,7 +5,6 @@ import teammates.common.util.AppUrl; import teammates.common.util.Const; import teammates.e2e.pageobjects.AdminHomePage; -import teammates.storage.sqlentity.AccountRequest; /** * SUT: {@link Const.WebPageURIs#ADMIN_HOME_PAGE}. @@ -47,31 +46,6 @@ public void testAll() { String failureMessage = homePage.getMessageForInstructor(1); assertTrue(failureMessage.contains( "\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not in the correct format.")); - - assertNotNull(BACKDOOR.getAccountRequest(email, institute)); - BACKDOOR.deleteAccountRequest(email, institute); - - ______TS("Failure case: Instructor is already registered"); - AccountRequest registeredAccountRequest = sqlTestData.accountRequests.get("AHome.instructor1OfCourse1"); - homePage.queueInstructorForAdding(registeredAccountRequest.getName(), - registeredAccountRequest.getEmail(), registeredAccountRequest.getInstitute()); - - homePage.addAllInstructors(); - - failureMessage = homePage.getMessageForInstructor(2); - assertTrue(failureMessage.contains("Cannot create account request as instructor has already registered.")); - - ______TS("Success case: Reset account request"); - - homePage.clickMoreInfoButtonForRegisteredInstructor(2); - homePage.clickResetAccountRequestLink(); - - successMessage = homePage.getMessageForInstructor(2); - assertTrue(successMessage.contains( - "Instructor \"" + registeredAccountRequest.getName() + "\" has been successfully created")); - - assertNull(BACKDOOR.getAccountRequest( - registeredAccountRequest.getEmail(), registeredAccountRequest.getInstitute()).getRegisteredAt()); } } diff --git a/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java index b73e82c0808..22089ac7cea 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminSearchPageE2ETest.java @@ -132,7 +132,7 @@ public void testAll() { searchPage.inputSearchContent(searchContent); searchPage.clickSearchButton(); searchPage.clickResetAccountRequestButton(accountRequest); - assertNull(BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()).getRegisteredAt()); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId()).getRegisteredAt()); ______TS("Typical case: Delete account request successful"); accountRequest = sqlTestData.accountRequests.get("unregisteredInstructor1"); @@ -141,7 +141,7 @@ public void testAll() { searchPage.inputSearchContent(searchContent); searchPage.clickSearchButton(); searchPage.clickDeleteAccountRequestButton(accountRequest); - assertNull(BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute())); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId())); } private String getExpectedStudentDetails(StudentAttributes student) { @@ -193,7 +193,7 @@ private String getExpectedInstructorManageAccountLink(InstructorAttributes instr @AfterClass public void classTeardown() { for (AccountRequest request : sqlTestData.accountRequests.values()) { - BACKDOOR.deleteAccountRequest(request.getEmail(), request.getInstitute()); + BACKDOOR.deleteAccountRequest(request.getId()); } } diff --git a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java index 3ada841ac59..7b953f7a50a 100644 --- a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java +++ b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java @@ -4,6 +4,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; +import java.util.UUID; import org.testng.ITestContext; import org.testng.annotations.AfterClass; @@ -329,7 +330,7 @@ protected String getKeyForStudent(StudentAttributes student) { @Override protected AccountRequestAttributes getAccountRequest(AccountRequestAttributes accountRequest) { - return BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + return BACKDOOR.getAccountRequest(UUID.fromString(accountRequest.getId())); } NotificationAttributes getNotification(String notificationId) { diff --git a/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java index d3dffb01c9f..421144a2da4 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorCourseJoinConfirmationPageE2ETest.java @@ -68,7 +68,7 @@ public void testAll() { ______TS("Click join link: valid account request key"); String regKey = BACKDOOR - .getRegKeyForAccountRequest("ICJoinConf.newinstr@gmail.tmt", "TEAMMATES Test Institute 1"); + .getRegKeyForAccountRequest(sqlTestData.accountRequests.get("ICJoinConf.newinstr").getId()); joinLink = createFrontendUrl(Const.WebPageURIs.JOIN_PAGE) .withIsCreatingAccount("true") diff --git a/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java new file mode 100644 index 00000000000..e4e4d8c3560 --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/AdminHomePageE2ETest.java @@ -0,0 +1,55 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.e2e.pageobjects.AdminHomePage; + +/** + * SUT: {@link Const.WebPageURIs#ADMIN_HOME_PAGE}. + */ +public class AdminHomePageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + // not needed + } + + @Test + @Override + public void testAll() { + AppUrl url = createFrontendUrl(Const.WebPageURIs.ADMIN_HOME_PAGE); + AdminHomePage homePage = loginAdminToPage(url, AdminHomePage.class); + + ______TS("Test adding instructors with both valid and invalid details"); + + String name = "AHPUiT Instrúctör WithPlusInEmail"; + String email = "AHPUiT+++_.instr1!@gmail.tmt"; + String institute = "TEAMMATES Test Institute 1"; + + homePage.queueInstructorForAdding(name, email, institute); + + String singleLineDetails = "Instructor With Invalid Email | invalidemail | TEAMMATES Test Institute 1"; + + homePage.queueInstructorForAdding(singleLineDetails); + + homePage.addAllInstructors(); + + String successMessage = homePage.getMessageForInstructor(0); + assertTrue(successMessage.contains( + "Instructor \"AHPUiT Instrúctör WithPlusInEmail\" has been successfully created")); + + String failureMessage = homePage.getMessageForInstructor(1); + assertTrue(failureMessage.contains( + "\"invalidemail\" is not acceptable to TEAMMATES as a/an email because it is not in the correct format.")); + + homePage.reloadPage(); + + ______TS("Verify that newly added instructor appears in account request table"); + + homePage.verifyInstructorInAccountRequestTable(name, email, institute); + + } + +} diff --git a/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java index 780b0f212fd..eb209578a7d 100644 --- a/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/sql/AdminSearchPageE2ETest.java @@ -5,8 +5,11 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.util.AppUrl; import teammates.common.util.Const; +import teammates.common.util.FieldValidator; +import teammates.common.util.StringHelperExtension; import teammates.e2e.pageobjects.AdminSearchPage; import teammates.e2e.util.TestProperties; import teammates.storage.sqlentity.AccountRequest; @@ -116,7 +119,7 @@ public void testAll() { searchPage.inputSearchContent(searchContent); searchPage.clickSearchButton(); searchPage.clickResetAccountRequestButton(accountRequest); - assertNull(BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()).getRegisteredAt()); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId()).getRegisteredAt()); ______TS("Typical case: Delete account request successful"); accountRequest = testData.accountRequests.get("unregisteredInstructor1"); @@ -125,14 +128,108 @@ public void testAll() { searchPage.inputSearchContent(searchContent); searchPage.clickSearchButton(); searchPage.clickDeleteAccountRequestButton(accountRequest); - assertNull(BACKDOOR.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute())); + assertNull(BACKDOOR.getAccountRequest(accountRequest.getId())); + + ______TS("Typical case: Edit account request successful"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickEditAccountRequestButton(accountRequest); + searchPage.fillInEditModalFields("Different name", accountRequest.getEmail(), + accountRequest.getInstitute(), "New comment"); + searchPage.clickSaveEditAccountRequestButton(); + accountRequest.setName("Different name"); + accountRequest.setComments("New comment"); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Typical case: View comment of account request"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickViewAccountRequestAndVerifyCommentsButton(accountRequest, "New comment"); + + ______TS("Edit account request with invalid details"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickEditAccountRequestButton(accountRequest); + searchPage.fillInEditModalFields(accountRequest.getName(), "invalid", + accountRequest.getInstitute(), "New comment"); + searchPage.clickSaveEditAccountRequestButton(); + String formattedErrorMessage = String.format("\"%s\" is not acceptable to TEAMMATES as a/an %s because it %s. " + + "An email address contains some text followed by one '@' sign followed by some more text, " + + "and should end with a top level domain address like .com. It cannot be longer than %d characters, " + + "cannot be empty and cannot contain spaces.", + "invalid", FieldValidator.EMAIL_FIELD_NAME, FieldValidator.REASON_INCORRECT_FORMAT, + FieldValidator.EMAIL_MAX_LENGTH); + searchPage.verifyStatusMessage(formattedErrorMessage); + + String name = StringHelperExtension.generateStringOfLength(FieldValidator.PERSON_NAME_MAX_LENGTH + 1); + + searchPage.clickEditAccountRequestButton(accountRequest); + searchPage.fillInEditModalFields(name, accountRequest.getEmail(), accountRequest.getInstitute(), "New comment"); + searchPage.clickSaveEditAccountRequestButton(); + formattedErrorMessage = String.format("\"%s\" is not acceptable to TEAMMATES as a/an %s because it %s. " + + "The value of a/an %s should be no longer than %d characters. It should not be empty.", + name, FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.REASON_TOO_LONG, + FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.PERSON_NAME_MAX_LENGTH); + searchPage.verifyStatusMessage(formattedErrorMessage); + + ______TS("Typical case: Approve account request successful"); + accountRequest = testData.accountRequests.get("unregisteredInstructor2"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickApproveAccountRequestButton(accountRequest); + accountRequest.setStatus(AccountRequestStatus.APPROVED); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Typical case: Reject account request successfully"); + accountRequest = testData.accountRequests.get("unregisteredInstructor3"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickRejectAccountRequestButton(accountRequest); + accountRequest.setStatus(AccountRequestStatus.REJECTED); + searchPage.verifyAccountRequestRowContent(accountRequest); + + ______TS("Reject account request with empty body"); + accountRequest = testData.accountRequests.get("unregisteredInstructor5"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickRejectAccountRequestWithReasonButton(accountRequest); + searchPage.fillInRejectionModalBody(""); + searchPage.clickConfirmRejectAccountRequest(); + searchPage.verifyStatusMessage("Please provide an email body for the rejection email."); + searchPage.closeRejectionModal(); + + ______TS("Typical case: Reject account request with reason successfully"); + accountRequest = testData.accountRequests.get("unregisteredInstructor4"); + searchContent = accountRequest.getEmail(); + searchPage.clearSearchBox(); + searchPage.inputSearchContent(searchContent); + searchPage.clickSearchButton(); + searchPage.clickRejectAccountRequestWithReasonButton(accountRequest); + accountRequest.setStatus(AccountRequestStatus.REJECTED); + searchPage.verifyAccountRequestRowContent(accountRequest); } private String getExpectedStudentDetails(Student student) { return String.format("%s [%s] (%s)", student.getCourse().getId(), student.getSection() == null - ? Const.DEFAULT_SECTION - : student.getSection().getName(), student.getTeam().getName()); + ? Const.DEFAULT_SECTION + : student.getSection().getName(), + student.getTeam().getName()); } private String getExpectedStudentHomePageLink(Student student) { @@ -179,7 +276,7 @@ private String getExpectedInstructorManageAccountLink(Instructor instructor) { @AfterClass public void classTeardown() { for (AccountRequest request : testData.accountRequests.values()) { - BACKDOOR.deleteAccountRequest(request.getEmail(), request.getInstitute()); + BACKDOOR.deleteAccountRequest(request.getId()); } } } diff --git a/src/e2e/java/teammates/e2e/cases/sql/RequestPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/sql/RequestPageE2ETest.java new file mode 100644 index 00000000000..4cb678a053f --- /dev/null +++ b/src/e2e/java/teammates/e2e/cases/sql/RequestPageE2ETest.java @@ -0,0 +1,52 @@ +package teammates.e2e.cases.sql; + +import org.testng.annotations.Test; + +import teammates.common.util.AppUrl; +import teammates.common.util.Const; +import teammates.common.util.EmailType; +import teammates.e2e.pageobjects.RequestPage; +import teammates.e2e.util.TestProperties; + +/** + * SUT: {@link Const.WebPageURIs#ACCOUNT_REQUEST_PAGE}. + */ +public class RequestPageE2ETest extends BaseE2ETestCase { + + @Override + protected void prepareTestData() { + // No test data needed + } + + @Test + @Override + protected void testAll() { + String name = "arf-test-name"; + String institution = "arf-test-institution"; + String country = "arf-test-country"; + String email = TestProperties.TEST_EMAIL; + String comments = "arf-test-comments"; + + AppUrl url = createFrontendUrl(Const.WebPageURIs.ACCOUNT_REQUEST_PAGE); + RequestPage requestPage = getNewPageInstance(url, RequestPage.class); + + ______TS("verify submission with comments"); + requestPage.clickAmInstructorButton(); + requestPage.fillForm(name, institution, country, email, comments); + requestPage.clickSubmitFormButton(); + requestPage.verifySubmittedInfo(name, institution, country, email, comments); + + String expectedEmailSubject = EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT.toString(); + verifyEmailSent(email, expectedEmailSubject); + + ______TS("verify submission without comments"); + requestPage = getNewPageInstance(url, RequestPage.class); + requestPage.clickAmInstructorButton(); + requestPage.fillForm(name, institution, country, email, ""); + requestPage.clickSubmitFormButton(); + requestPage.verifySubmittedInfo(name, institution, country, email, ""); + + expectedEmailSubject = EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT.toString(); + verifyEmailSent(email, expectedEmailSubject); + } +} diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java index 7e40f8ebef4..9316f042077 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminHomePage.java @@ -1,5 +1,7 @@ package teammates.e2e.pageobjects; +import static org.junit.Assert.assertNotNull; + import java.util.List; import org.openqa.selenium.By; @@ -12,6 +14,9 @@ * Represents the admin home page of the website. */ public class AdminHomePage extends AppPage { + private static final int ACCOUNT_REQUEST_COL_NAME = 1; + private static final int ACCOUNT_REQUEST_COL_EMAIL = 2; + private static final int ACCOUNT_REQUEST_COL_INSTITUTE = 4; @FindBy(id = "instructor-details-single-line") private WebElement detailsSingleLineTextBox; @@ -95,4 +100,29 @@ public void clickResetAccountRequestLink() { List okButtons = browser.driver.findElements(By.className("modal-btn-ok")); clickDismissModalButtonAndWaitForModalHidden(okButtons.get(1)); // Second modal is confirmation modal } + + public String removeSpanFromText(String text) { + return text.replace("", "").replace("", ""); + } + + public WebElement getAccountRequestRow(String name, String email, String institute) { + List rows = browser.driver.findElements(By.cssSelector("tm-account-request-table tbody tr")); + for (WebElement row : rows) { + List columns = row.findElements(By.tagName("td")); + if (removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_NAME - 1) + .getAttribute("innerHTML")).contains(name) + && removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_EMAIL - 1) + .getAttribute("innerHTML")).contains(email) + && removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_INSTITUTE - 1) + .getAttribute("innerHTML")).contains(institute)) { + return row; + } + } + return null; + } + + public void verifyInstructorInAccountRequestTable(String name, String email, String institute) { + WebElement row = getAccountRequestRow(name, email, institute); + assertNotNull(row); + } } diff --git a/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java b/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java index 005b98a026b..289cbb13148 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AdminSearchPage.java @@ -41,9 +41,9 @@ public class AdminSearchPage extends AppPage { private static final int ACCOUNT_REQUEST_COL_NAME = 1; private static final int ACCOUNT_REQUEST_COL_EMAIL = 2; - private static final int ACCOUNT_REQUEST_COL_INSTITUTE = 3; - private static final int ACCOUNT_REQUEST_COL_CREATED_AT = 4; - private static final int ACCOUNT_REQUEST_COL_REGISTERED_AT = 5; + private static final int ACCOUNT_REQUEST_COL_INSTITUTE = 4; + private static final int ACCOUNT_REQUEST_COL_CREATED_AT = 5; + private static final int ACCOUNT_REQUEST_COL_REGISTERED_AT = 6; private static final String EXPANDED_ROWS_HEADER_EMAIL = "Email"; private static final String EXPANDED_ROWS_HEADER_COURSE_JOIN_LINK = "Course Join Link"; @@ -370,7 +370,7 @@ public void resetInstructorGoogleId(InstructorAttributes instructor) { public WebElement getAccountRequestRow(AccountRequestAttributes accountRequest) { String email = accountRequest.getEmail(); String institute = accountRequest.getInstitute(); - List rows = browser.driver.findElements(By.cssSelector("#search-table-account-request tbody tr")); + List rows = browser.driver.findElements(By.cssSelector("tm-account-request-table tbody tr")); for (WebElement row : rows) { List columns = row.findElements(By.tagName("td")); if (removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_EMAIL - 1) @@ -386,7 +386,7 @@ && removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_INSTITUTE - 1) public WebElement getAccountRequestRow(AccountRequest accountRequest) { String email = accountRequest.getEmail(); String institute = accountRequest.getInstitute(); - List rows = browser.driver.findElements(By.cssSelector("#search-table-account-request tbody tr")); + List rows = browser.driver.findElements(By.cssSelector("tm-account-request-table tbody tr")); for (WebElement row : rows) { List columns = row.findElements(By.tagName("td")); if (removeSpanFromText(columns.get(ACCOUNT_REQUEST_COL_EMAIL - 1) @@ -439,6 +439,111 @@ public void clickDeleteAccountRequestButton(AccountRequest accountRequest) { waitForPageToLoad(); } + public void clickApproveAccountRequestButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + waitForElementPresence(By.cssSelector("[id^='approve-account-request-']")); + WebElement approveButton = accountRequestRow.findElement(By.cssSelector("[id^='approve-account-request-']")); + waitForElementToBeClickable(approveButton); + approveButton.click(); + waitForPageToLoad(); + } + + public void clickRejectAccountRequestButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement rejectButton = accountRequestRow.findElement(By.cssSelector("[id^='reject-account-request-']")); + rejectButton.click(); + waitForPageToLoad(); + WebElement rejectWithoutReasonButton = browser.driver.findElement(By.cssSelector("[id^='reject-request-']")); + rejectWithoutReasonButton.click(); + waitForPageToLoad(); + } + + public void clickRejectAccountRequestWithReasonButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement rejectButton = accountRequestRow.findElement(By.cssSelector("[id^='reject-account-request-']")); + rejectButton.click(); + waitForPageToLoad(); + WebElement rejectWithReasonButton = browser.driver.findElement(By.cssSelector("[id^='reject-request-with-reason']")); + waitForElementToBeClickable(rejectWithReasonButton); + rejectWithReasonButton.click(); + waitForPageToLoad(); + waitForElementPresence(By.cssSelector("tm-reject-with-reason-modal")); + } + + public void fillInRejectionModalTitle(String title) { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement titleInput = rejectionModal.findElement(By.cssSelector("[id^='rejection-reason-title']")); + titleInput.clear(); + titleInput.sendKeys(title); + } + + public void fillInRejectionModalBody(String body) { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement bodyInput = rejectionModal.findElement(By.cssSelector("tm-rich-text-editor")); + clearRichTextEditor(bodyInput); + writeToRichTextEditor(bodyInput, body); + } + + public void clickConfirmRejectAccountRequest() { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement clickReject = rejectionModal.findElement(By.cssSelector("[id^='btn-confirm-reject-request']")); + clickReject.click(); + waitForPageToLoad(); + } + + public void closeRejectionModal() { + WebElement rejectionModal = browser.driver.findElement(By.cssSelector("tm-reject-with-reason-modal")); + WebElement clickCancel = rejectionModal.findElement(By.cssSelector("[id^='btn-cancel-reject-request']")); + clickCancel.click(); + waitForPageToLoad(); + } + + public void clickEditAccountRequestButton(AccountRequest accountRequest) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement editButton = accountRequestRow.findElement(By.cssSelector("[id^='edit-account-request-']")); + editButton.click(); + waitForElementPresence(By.cssSelector("tm-edit-request-modal")); + } + + public void fillInEditModalFields(String name, String email, String institute, String comments) { + waitForElementPresence(By.cssSelector("tm-edit-request-modal")); + + WebElement editModal = browser.driver.findElement(By.cssSelector("tm-edit-request-modal")); + WebElement nameInput = editModal.findElement(By.cssSelector("[id^='request-name']")); + nameInput.clear(); + nameInput.sendKeys(name); + + WebElement emailInput = editModal.findElement(By.cssSelector("[id^='request-email']")); + emailInput.clear(); + emailInput.sendKeys(email); + + WebElement instituteInput = editModal.findElement(By.cssSelector("[id^='request-institution']")); + instituteInput.clear(); + instituteInput.sendKeys(institute); + + WebElement commentsInput = editModal.findElement(By.cssSelector("[id^='request-comments']")); + commentsInput.clear(); + commentsInput.sendKeys(comments); + } + + public void clickSaveEditAccountRequestButton() { + WebElement editModal = browser.driver.findElement(By.cssSelector("tm-edit-request-modal")); + WebElement saveButton = editModal.findElement(By.cssSelector("[id^='btn-confirm-edit-request']")); + saveButton.click(); + waitForPageToLoad(); + } + + public void clickViewAccountRequestAndVerifyCommentsButton(AccountRequest accountRequest, String comments) { + WebElement accountRequestRow = getAccountRequestRow(accountRequest); + WebElement viewCommentsButton = accountRequestRow.findElement(By.cssSelector("[id^='view-account-request-']")); + viewCommentsButton.click(); + waitForElementVisibility(By.className("modal-btn-ok")); + WebElement modal = browser.driver.findElement(By.className("modal-body")); + String actualComments = modal.findElement(By.tagName("div")).getText(); + assertEquals("Comment: " + comments, actualComments); + waitForConfirmationModalAndClickOk(); + } + public void clickResetAccountRequestButton(AccountRequestAttributes accountRequest) { WebElement accountRequestRow = getAccountRequestRow(accountRequest); WebElement deleteButton = accountRequestRow.findElement(By.cssSelector("[id^='reset-account-request-']")); diff --git a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java index c8d5911e57f..c52330fd928 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java @@ -395,6 +395,13 @@ protected void writeToRichTextEditor(WebElement editor, String text) { ((JavascriptExecutor) browser.driver).executeAsyncScript(WRITE_TO_TINYMCE_SCRIPT, id, text); } + /** + * Clear existing text in the editor. + */ + protected void clearRichTextEditor(WebElement editor) { + writeToRichTextEditor(editor, ""); + } + /** * Select the option, if it is not already selected. * No action taken if it is already selected. diff --git a/src/e2e/java/teammates/e2e/pageobjects/RequestPage.java b/src/e2e/java/teammates/e2e/pageobjects/RequestPage.java new file mode 100644 index 00000000000..05ef23a56e0 --- /dev/null +++ b/src/e2e/java/teammates/e2e/pageobjects/RequestPage.java @@ -0,0 +1,71 @@ +package teammates.e2e.pageobjects; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * Page Object Model for account request form page. + */ +public class RequestPage extends AppPage { + + @FindBy(id = "btn-am-instructor") + private WebElement amInstructorButton; + + @FindBy(id = "name") + private WebElement nameBox; + + @FindBy(id = "institution") + private WebElement institutionBox; + + @FindBy(id = "country") + private WebElement countryBox; + + @FindBy(id = "email") + private WebElement emailBox; + + @FindBy(id = "comments") + private WebElement commentsBox; + + @FindBy(id = "submit-button") + private WebElement submitButton; + + public RequestPage(Browser browser) { + super(browser); + } + + @Override + protected boolean containsExpectedPageContents() { + return getPageTitle().contains("Request for an Instructor Account"); + } + + public void clickAmInstructorButton() { + click(amInstructorButton); + waitForPageToLoad(); + } + + public void fillForm(String name, String institution, String country, String email, String comments) { + fillTextBox(nameBox, name); + fillTextBox(institutionBox, institution); + fillTextBox(countryBox, country); + fillTextBox(emailBox, email); + fillTextBox(commentsBox, comments); + } + + public void clickSubmitFormButton() { + click(submitButton); + waitForPageToLoad(); + } + + public void verifySubmittedInfo(String name, String institution, String country, String email, String comments) { + WebElement table = browser.driver.findElement(By.className("table")); + String[][] expected = { + { name }, + { institution }, + { country }, + { email }, + { comments }, + }; + verifyTableBodyValues(table, expected); + } +} diff --git a/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json b/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json index 94b28ae6ad2..3a36ff511d8 100644 --- a/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json +++ b/src/e2e/resources/data/AdminSearchPageE2ESqlTest.json @@ -38,7 +38,36 @@ "name": "Typical Instructor Name", "email": "ASearch.unregisteredinstructor1@gmail.tmt", "institute": "TEAMMATES Test Institute 1", - "createdAt": "2011-01-01T00:00:00Z" + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor2": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor2@gmail.tmt", + "institute": "TEAMMATES Test Institute 2", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor3": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor3@gmail.tmt", + "institute": "TEAMMATES Test Institute 3", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor4": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor4@gmail.tmt", + "institute": "TEAMMATES Test Institute 4", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" + }, + "unregisteredInstructor5": { + "name": "Typical Instructor Name", + "email": "ASearch.unregisteredinstructor5@gmail.tmt", + "institute": "TEAMMATES Test Institute 5", + "createdAt": "2011-01-01T00:00:00Z", + "status": "PENDING" } }, "courses": { diff --git a/src/e2e/resources/testng-e2e-sql.xml b/src/e2e/resources/testng-e2e-sql.xml index 61cc506ff93..42881b3a3e7 100644 --- a/src/e2e/resources/testng-e2e-sql.xml +++ b/src/e2e/resources/testng-e2e-sql.xml @@ -13,8 +13,10 @@ + + diff --git a/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java b/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java index c449f3358a0..c4b128ddbfd 100644 --- a/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java +++ b/src/it/java/teammates/it/sqllogic/core/AccountRequestsLogicIT.java @@ -1,9 +1,11 @@ package teammates.it.sqllogic.core; import java.time.Instant; +import java.util.UUID; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; @@ -19,6 +21,23 @@ public class AccountRequestsLogicIT extends BaseTestCaseWithSqlDatabaseAccess { private AccountRequestsLogic accountRequestsLogic = AccountRequestsLogic.inst(); + @Test + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + assertNull(actualAccountRequest); + } + + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() throws InvalidParametersException { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + accountRequestsLogic.createAccountRequest(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + assertEquals(expectedAccountRequest, actualAccountRequest); + } + @Test public void testResetAccountRequest() throws EntityAlreadyExistsException, InvalidParametersException, EntityDoesNotExistException { @@ -28,31 +47,34 @@ public void testResetAccountRequest() String name = "name lee"; String email = "email@gmail.com"; String institute = "institute"; + AccountRequestStatus status = AccountRequestStatus.PENDING; + String comments = "comments"; - AccountRequest toReset = accountRequestsLogic.createAccountRequest(name, email, institute); + AccountRequest toReset = accountRequestsLogic.createAccountRequest(name, email, institute, status, comments); AccountRequestsDb accountRequestsDb = AccountRequestsDb.inst(); toReset.setRegisteredAt(Instant.now()); - toReset = accountRequestsDb.getAccountRequest(email, institute); + UUID id = toReset.getId(); + toReset = accountRequestsDb.getAccountRequest(id); assertNotNull(toReset); assertNotNull(toReset.getRegisteredAt()); ______TS("success: reset account request that already exists"); - AccountRequest resetted = accountRequestsLogic.resetAccountRequest(email, institute); + AccountRequest resetted = accountRequestsLogic.resetAccountRequest(id); assertNull(resetted.getRegisteredAt()); ______TS("success: test delete account request"); - accountRequestsLogic.deleteAccountRequest(email, institute); + accountRequestsLogic.deleteAccountRequest(toReset.getId()); - assertNull(accountRequestsLogic.getAccountRequest(email, institute)); + assertNull(accountRequestsLogic.getAccountRequest(toReset.getId())); ______TS("failure: reset account request that does not exist"); assertThrows(EntityDoesNotExistException.class, - () -> accountRequestsLogic.resetAccountRequest(name, institute)); + () -> accountRequestsLogic.resetAccountRequest(id)); } } diff --git a/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java b/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java index f8571037632..079f95b13e7 100644 --- a/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java +++ b/src/it/java/teammates/it/sqllogic/core/DataBundleLogicIT.java @@ -8,6 +8,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.InstructorPermissionRole; import teammates.common.datatransfer.InstructorPrivileges; @@ -62,7 +63,7 @@ public void testCreateDataBundle_typicalValues_createdCorrectly() throws Excepti AccountRequest actualAccountRequest = dataBundle.accountRequests.get("instructor1"); AccountRequest expectedAccountRequest = new AccountRequest("instr1@teammates.tmt", "Instructor 1", - "TEAMMATES Test Institute 1"); + "TEAMMATES Test Institute 1", AccountRequestStatus.REGISTERED, "These are some comments."); expectedAccountRequest.setId(actualAccountRequest.getId()); expectedAccountRequest.setRegisteredAt(Instant.parse("2015-02-14T00:00:00Z")); expectedAccountRequest.setRegistrationKey(actualAccountRequest.getRegistrationKey()); diff --git a/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java b/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java index 8af4c8065df..6807e43a9b4 100644 --- a/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java +++ b/src/it/java/teammates/it/storage/sqlapi/AccountRequestsDbIT.java @@ -1,11 +1,13 @@ package teammates.it.storage.sqlapi; import java.util.List; +import java.util.UUID; import org.testng.annotations.Test; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; import teammates.storage.sqlapi.AccountRequestsDb; import teammates.storage.sqlentity.AccountRequest; @@ -21,13 +23,13 @@ public class AccountRequestsDbIT extends BaseTestCaseWithSqlDatabaseAccess { public void testCreateReadDeleteAccountRequest() throws Exception { ______TS("Create account request, does not exists, succeeds"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); - ______TS("Read account request using the given email and institute"); + ______TS("Read account request using the given ID"); - AccountRequest actualAccReqEmalAndInstitute = - accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actualAccReqEmalAndInstitute = accountRequestDb.getAccountRequest(accountRequest.getId()); verifyEquals(accountRequest, actualAccReqEmalAndInstitute); ______TS("Read account request using the given registration key"); @@ -51,29 +53,49 @@ public void testCreateReadDeleteAccountRequest() throws Exception { accountRequest.getCreatedAt().minusMillis(2000)); assertEquals(0, actualAccReqCreatedAtOutside.size()); - ______TS("Create acccount request, already exists, execption thrown"); + ______TS("Create account request, same email address and institute already exist, creates successfully"); AccountRequest identicalAccountRequest = - new AccountRequest("test@gmail.com", "name", "institute"); + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); assertNotSame(accountRequest, identicalAccountRequest); - assertThrows(EntityAlreadyExistsException.class, - () -> accountRequestDb.createAccountRequest(identicalAccountRequest)); + accountRequestDb.createAccountRequest(identicalAccountRequest); + AccountRequest actualIdenticalAccountRequest = + accountRequestDb.getAccountRequestByRegistrationKey(identicalAccountRequest.getRegistrationKey()); + verifyEquals(identicalAccountRequest, actualIdenticalAccountRequest); ______TS("Delete account request that was created"); accountRequestDb.deleteAccountRequest(accountRequest); AccountRequest actualAccountRequest = - accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + accountRequestDb.getAccountRequestByRegistrationKey(accountRequest.getRegistrationKey()); assertNull(actualAccountRequest); } + @Test + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + assertNull(actualAccountRequest); + } + + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() throws InvalidParametersException { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + accountRequestDb.createAccountRequest(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + assertEquals(expectedAccountRequest, actualAccountRequest); + } + @Test public void testUpdateAccountRequest() throws Exception { ______TS("Update account request, does not exists, exception thrown"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); assertThrows(EntityDoesNotExistException.class, () -> accountRequestDb.updateAccountRequest(accountRequest)); @@ -84,8 +106,7 @@ public void testUpdateAccountRequest() throws Exception { accountRequest.setName("new account request name"); accountRequestDb.updateAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest( - accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); verifyEquals(accountRequest, actual); } @@ -95,11 +116,12 @@ public void testSqlInjectionInCreateAccountRequestEmailField() throws Exception // Attempt to use SQL commands in email field String email = "email'/**/OR/**/1=1/**/@gmail.com"; - AccountRequest accountRequest = new AccountRequest(email, "name", "institute"); + AccountRequest accountRequest = + new AccountRequest(email, "name", "institute", AccountRequestStatus.PENDING, "comments"); // The system should treat the input as a plain text string accountRequestDb.createAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(email, actual.getEmail()); } @@ -109,11 +131,12 @@ public void testSqlInjectionInCreateAccountRequestNameField() throws Exception { // Attempt to use SQL commands in name field String name = "name'; SELECT * FROM account_requests; --"; - AccountRequest accountRequest = new AccountRequest("test@gmail.com", name, "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", name, "institute", AccountRequestStatus.PENDING, "comments"); // The system should treat the input as a plain text string accountRequestDb.createAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(name, actual.getName()); } @@ -123,34 +146,36 @@ public void testSqlInjectionInCreateAccountRequestInstituteField() throws Except // Attempt to use SQL commands in institute field String institute = "institute'; DROP TABLE account_requests; --"; - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", institute); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", institute, AccountRequestStatus.PENDING, "comments"); // The system should treat the input as a plain text string accountRequestDb.createAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), institute); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(institute, actual.getInstitute()); } @Test - public void testSqlInjectionInGetAccountRequest() throws Exception { - ______TS("SQL Injection test in getAccountRequest"); - - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - accountRequestDb.createAccountRequest(accountRequest); + public void testSqlInjectionInCreateAccountRequestCommentsField() throws Exception { + ______TS("SQL Injection test in comments field"); - String instituteInjection = "institute'; DROP TABLE account_requests; --"; - AccountRequest actualInjection = accountRequestDb.getAccountRequest(accountRequest.getEmail(), instituteInjection); - assertNull(actualInjection); + // Attempt to use SQL commands in comments field + String comments = "comment'; DROP TABLE account_requests; --"; + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, comments); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); - assertEquals(accountRequest, actual); + // The system should treat the input as a plain text string + accountRequestDb.createAccountRequest(accountRequest); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); + assertEquals(comments, actual.getComments()); } @Test public void testSqlInjectionInGetAccountRequestByRegistrationKey() throws Exception { ______TS("SQL Injection test in getAccountRequestByRegistrationKey"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String regKeyInjection = "regKey'; DROP TABLE account_requests; --"; @@ -161,18 +186,35 @@ public void testSqlInjectionInGetAccountRequestByRegistrationKey() throws Except assertEquals(accountRequest, actual); } + @Test + public void testSqlInjectionInGetApprovedAccountRequestsForEmail() throws Exception { + ______TS("SQL Injection test in getApprovedAccountRequestsForEmail"); + + String email = "test@gmail.com"; + AccountRequest accountRequest = + new AccountRequest(email, "name", "institute", AccountRequestStatus.APPROVED, "comments"); + accountRequestDb.createAccountRequest(accountRequest); + + // Attempt to use SQL commands in email field + String emailInjection = "email'/**/OR/**/1=1/**/@gmail.com"; + List actualInjection = accountRequestDb.getApprovedAccountRequestsForEmail(emailInjection); + // The system should treat the input as a plain text string + assertEquals(0, actualInjection.size()); + } + @Test public void testSqlInjectionInUpdateAccountRequest() throws Exception { ______TS("SQL Injection test in updateAccountRequest"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String nameInjection = "newName'; DROP TABLE account_requests; --"; accountRequest.setName(nameInjection); accountRequestDb.updateAccountRequest(accountRequest); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(accountRequest, actual); } @@ -180,16 +222,18 @@ public void testSqlInjectionInUpdateAccountRequest() throws Exception { public void testSqlInjectionInDeleteAccountRequest() throws Exception { ______TS("SQL Injection test in deleteAccountRequest"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String emailInjection = "email'/**/OR/**/1=1/**/@gmail.com"; String nameInjection = "name'; DROP TABLE account_requests; --"; String instituteInjection = "institute'; DROP TABLE account_requests; --"; - AccountRequest accountRequestInjection = new AccountRequest(emailInjection, nameInjection, instituteInjection); + AccountRequest accountRequestInjection = new AccountRequest(emailInjection, nameInjection, instituteInjection, + AccountRequestStatus.PENDING, "comments"); accountRequestDb.deleteAccountRequest(accountRequestInjection); - AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(accountRequest, actual); } @@ -197,14 +241,15 @@ public void testSqlInjectionInDeleteAccountRequest() throws Exception { public void testSqlInjectionSearchAccountRequestsInWholeSystem() throws Exception { ______TS("SQL Injection test in searchAccountRequestsInWholeSystem"); - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); String searchInjection = "institute'; DROP TABLE account_requests; --"; List actualInjection = accountRequestDb.searchAccountRequestsInWholeSystem(searchInjection); assertEquals(0, actualInjection.size()); - AccountRequest actual = accountRequestDb.getAccountRequest("test@gmail.com", "institute"); + AccountRequest actual = accountRequestDb.getAccountRequest(accountRequest.getId()); assertEquals(accountRequest, actual); } } diff --git a/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java b/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java index db64c17c2ab..a9b196eafc8 100644 --- a/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java +++ b/src/it/java/teammates/it/storage/sqlsearch/AccountRequestSearchIT.java @@ -89,6 +89,16 @@ public void allTests() throws Exception { results = accountRequestsDb.searchAccountRequestsInWholeSystem("\"TEAMMATES Test Institute 2\""); verifySearchResults(results, unregisteredInstructor2); + ______TS("success: search for account requests; account requests should be searchable by their comments"); + + results = accountRequestsDb.searchAccountRequestsInWholeSystem("Comments for account request from instructor2"); + verifySearchResults(results, ins2General); + + ______TS("success: search for account requests; account requests should be searchable by their status"); + + results = accountRequestsDb.searchAccountRequestsInWholeSystem("registered"); + verifySearchResults(results, ins2General); + ______TS("success: search for account requests; unregistered account requests should be searchable"); results = accountRequestsDb.searchAccountRequestsInWholeSystem("\"unregisteredinstructor1@gmail.tmt\""); diff --git a/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java b/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java index f6f5adc72f5..83e1b4399c4 100644 --- a/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java +++ b/src/it/java/teammates/it/test/BaseTestCaseWithSqlDatabaseAccess.java @@ -257,7 +257,7 @@ private BaseEntity getEntity(BaseEntity entity) { return logic.getNotification(((Notification) entity).getId()); } else if (entity instanceof AccountRequest) { AccountRequest accountRequest = (AccountRequest) entity; - return logic.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + return logic.getAccountRequest(accountRequest.getId()); } else if (entity instanceof Instructor) { return logic.getInstructor(((Instructor) entity).getId()); } else if (entity instanceof Student) { diff --git a/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java b/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java index a90fb7c9421..6ae1e9e51c4 100644 --- a/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/AccountRequestSearchIndexingWorkerActionIT.java @@ -1,6 +1,7 @@ package teammates.it.ui.webapi; import java.util.List; +import java.util.UUID; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -46,6 +47,7 @@ public void testExecute() throws Exception { } AccountRequest accountRequest = typicalBundle.accountRequests.get("instructor1"); + UUID accountRequestId = accountRequest.getId(); ______TS("account request not yet indexed should not be searchable"); @@ -56,8 +58,7 @@ public void testExecute() throws Exception { ______TS("account request indexed should be searchable"); String[] submissionParams = new String[] { - ParamsNames.INSTRUCTOR_EMAIL, accountRequest.getEmail(), - ParamsNames.INSTRUCTOR_INSTITUTION, accountRequest.getInstitute(), + ParamsNames.ACCOUNT_REQUEST_ID, accountRequestId.toString(), }; AccountRequestSearchIndexingWorkerAction action = getAction(submissionParams); diff --git a/src/it/java/teammates/it/ui/webapi/BaseActionIT.java b/src/it/java/teammates/it/ui/webapi/BaseActionIT.java index 950de8655e7..e489db0faf1 100644 --- a/src/it/java/teammates/it/ui/webapi/BaseActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/BaseActionIT.java @@ -22,6 +22,7 @@ import teammates.common.util.Config; import teammates.common.util.Const; import teammates.common.util.EmailWrapper; +import teammates.common.util.HibernateUtil; import teammates.common.util.JsonUtils; import teammates.it.test.BaseTestCaseWithSqlDatabaseAccess; import teammates.logic.api.MockEmailSender; @@ -169,6 +170,14 @@ protected void loginAsAdmin() { assertTrue(user.isAdmin); } + /** + * Logs in the user to the test environment as an admin. + */ + protected void loginAsAdminWithTransaction() { + UserInfo user = mockUserProvision.loginAsAdminWithTransaction(Config.APP_ADMINS.get(0)); + assertTrue(user.isAdmin); + } + /** * Logs in the user to the test environment as an unregistered user * (without any right). @@ -180,6 +189,17 @@ protected void loginAsUnregistered(String userId) { assertFalse(user.isAdmin); } + /** + * Logs in the user to the test environment as an unregistered user + * (without any right). + */ + protected void loginAsUnregisteredWithTransaction(String userId) { + UserInfo user = mockUserProvision.loginUserWithTransaction(userId); + assertFalse(user.isStudent); + assertFalse(user.isInstructor); + assertFalse(user.isAdmin); + } + /** * Logs in the user to the test environment as an instructor * (without admin rights or student rights). @@ -191,6 +211,17 @@ protected void loginAsInstructor(String userId) { assertFalse(user.isAdmin); } + /** + * Logs in the user to the test environment as an instructor + * (without admin rights or student rights). + */ + protected void loginAsInstructorWithTransaction(String userId) { + UserInfo user = mockUserProvision.loginUserWithTransaction(userId); + assertFalse(user.isStudent); + assertTrue(user.isInstructor); + assertFalse(user.isAdmin); + } + /** * Logs in the user to the test environment as a student * (without admin rights or instructor rights). @@ -202,6 +233,17 @@ protected void loginAsStudent(String userId) { assertFalse(user.isAdmin); } + /** + * Logs in the user to the test environment as a student + * (without admin rights or instructor rights). + */ + protected void loginAsStudentWithTransaction(String userId) { + UserInfo user = mockUserProvision.loginUserWithTransaction(userId); + assertTrue(user.isStudent); + assertFalse(user.isInstructor); + assertFalse(user.isAdmin); + } + /** * Logs in the user to the test environment as a student-instructor (without * admin rights). @@ -267,6 +309,24 @@ void verifyOnlyAdminCanAccess(Course course, String... params) verifyAccessibleForAdmin(params); } + void verifyOnlyAdminCanAccessWithTransaction(String... params) + throws InvalidParametersException, EntityAlreadyExistsException { + HibernateUtil.beginTransaction(); + Course course = getTypicalCourse(); + course = logic.createCourse(course); + HibernateUtil.commitTransaction(); + + verifyInaccessibleWithoutLogin(params); + verifyInaccessibleForUnregisteredUsersWithTransaction(params); + verifyInaccessibleForStudentsWithTransaction(course, params); + verifyInaccessibleForInstructorsWithTransaction(course, params); + verifyAccessibleForAdminWithTransaction(params); + + HibernateUtil.beginTransaction(); + logic.deleteCourseCascade(course.getId()); + HibernateUtil.commitTransaction(); + } + void verifyOnlyInstructorsCanAccess(Course course, String... params) throws InvalidParametersException, EntityAlreadyExistsException { verifyInaccessibleWithoutLogin(params); @@ -329,6 +389,14 @@ void verifyInaccessibleForUnregisteredUsers(String... params) { verifyCannotAccess(params); } + void verifyInaccessibleForUnregisteredUsersWithTransaction(String... params) { + ______TS("Non-registered users cannot access"); + + String unregUserId = "unreg.user"; + loginAsUnregisteredWithTransaction(unregUserId); + verifyCannotAccess(params); + } + void verifyAccessibleForAdmin(String... params) { ______TS("Admin can access"); @@ -336,6 +404,13 @@ void verifyAccessibleForAdmin(String... params) { verifyCanAccess(params); } + void verifyAccessibleForAdminWithTransaction(String... params) { + ______TS("Admin can access"); + + loginAsAdminWithTransaction(); + verifyCanAccess(params); + } + void verifyInaccessibleForAdmin(String... params) { ______TS("Admin cannot access"); @@ -353,6 +428,21 @@ void verifyInaccessibleForStudents(Course course, String... params) } + void verifyInaccessibleForStudentsWithTransaction(Course course, String... params) + throws InvalidParametersException, EntityAlreadyExistsException { + ______TS("Students cannot access"); + HibernateUtil.beginTransaction(); + Student student = createTypicalStudent(course, "InaccessibleForStudents@teammates.tmt"); + HibernateUtil.commitTransaction(); + + loginAsStudentWithTransaction(student.getAccount().getGoogleId()); + verifyCannotAccess(params); + + HibernateUtil.beginTransaction(); + logic.deleteAccountCascade(student.getAccount().getGoogleId()); + HibernateUtil.commitTransaction(); + } + void verifyInaccessibleForInstructors(Course course, String... params) throws InvalidParametersException, EntityAlreadyExistsException { ______TS("Instructors cannot access"); @@ -363,6 +453,21 @@ void verifyInaccessibleForInstructors(Course course, String... params) } + void verifyInaccessibleForInstructorsWithTransaction(Course course, String... params) + throws InvalidParametersException, EntityAlreadyExistsException { + ______TS("Instructors cannot access"); + HibernateUtil.beginTransaction(); + Instructor instructor = createTypicalInstructor(course, "InaccessibleForInstructors@teammates.tmt"); + HibernateUtil.commitTransaction(); + + loginAsInstructorWithTransaction(instructor.getAccount().getGoogleId()); + verifyCannotAccess(params); + + HibernateUtil.beginTransaction(); + logic.deleteAccountCascade(instructor.getAccount().getGoogleId()); + HibernateUtil.commitTransaction(); + } + void verifyAccessibleForAdminToMasqueradeAsInstructor( Instructor instructor, String[] submissionParams) { ______TS("admin can access"); @@ -738,5 +843,4 @@ private Student createTypicalStudent(Course course, String email) } return student; } - } diff --git a/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java b/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java index 9bf4c3b76fc..54df60a0234 100644 --- a/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/CreateAccountActionIT.java @@ -75,7 +75,7 @@ protected void testExecute() throws InvalidParametersException, EntityAlreadyExi ______TS("Normal case with valid timezone"); String timezone = "Asia/Singapore"; - AccountRequest accountRequest = logic.getAccountRequest(email, institute); + AccountRequest accountRequest = logic.getAccountRequest(accReq.getId()); String[] params = new String[] { Const.ParamsNames.REGKEY, accountRequest.getRegistrationKey(), @@ -118,10 +118,9 @@ protected void testExecute() throws InvalidParametersException, EntityAlreadyExi accReq = typicalBundle.accountRequests.get("unregisteredInstructor2"); email = accReq.getEmail(); - institute = accReq.getInstitute(); timezone = "InvalidTimezone"; - accountRequest = logic.getAccountRequest(email, institute); + accountRequest = logic.getAccountRequest(accReq.getId()); params = new String[] { Const.ParamsNames.REGKEY, accountRequest.getRegistrationKey(), diff --git a/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java b/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java index dcb1c279ae3..bc1ee01ad0f 100644 --- a/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/CreateAccountRequestActionIT.java @@ -1,15 +1,21 @@ package teammates.it.ui.webapi; +import java.util.List; + +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.InvalidParametersException; import teammates.common.util.Const; import teammates.common.util.EmailType; import teammates.common.util.EmailWrapper; import teammates.common.util.HibernateUtil; import teammates.storage.sqlentity.AccountRequest; -import teammates.ui.output.JoinLinkData; +import teammates.ui.output.AccountRequestData; import teammates.ui.request.AccountCreateRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; import teammates.ui.webapi.CreateAccountRequestAction; import teammates.ui.webapi.JsonResult; @@ -36,36 +42,214 @@ protected void setUp() { } @Override - @Test protected void testExecute() throws Exception { - // This is a minimal test; other cases are not tested due to upcoming changes in behaviour. + // This is separated into different test methods. + } + + @Test + void testExecute_nullEmail_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + assertEquals("email cannot be null", ihrbException.getMessage()); + } + + @Test + void testExecute_nullName_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + assertEquals("name cannot be null", ihrbException.getMessage()); + } + + @Test + void testExecute_nullInstitute_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + assertEquals("institute cannot be null", ihrbException.getMessage()); + } + + @Test + void testExecute_invalidEmail_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("invalid email address"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + String expectedMessage = "\"invalid email address\" is not acceptable to TEAMMATES as a/an email because it is not " + + "in the correct format. An email address contains some text followed by one '@' sign followed by some " + + "more text, and should end with a top level domain address like .com. It cannot be longer than 254 " + + "characters, cannot be empty and cannot contain spaces."; + assertEquals(expectedMessage, ihrbException.getMessage()); + } + + @Test + void testExecute_invalidName_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Pau| Atreides"); + request.setInstructorInstitution("House Atreides"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + String expectedMessage = "\"Pau| Atreides\" is not acceptable to TEAMMATES as a/an person name because it contains " + + "invalid characters. A/An person name must start with an alphanumeric character, and cannot contain any " + + "vertical bar (|) or percent sign (%)."; + assertEquals(expectedMessage, ihrbException.getMessage()); + } + + @Test + void testExecute_invalidInstitute_throwsInvalidHttpRequestBodyException() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreide%"); + InvalidHttpRequestBodyException ihrbException = verifyHttpRequestBodyFailure(request); + String expectedMessage = "\"House Atreide%\" is not acceptable to TEAMMATES as a/an institute name because it " + + "contains invalid characters. A/An institute name must start with an alphanumeric character, and cannot " + + "contain any vertical bar (|) or percent sign (%)."; + assertEquals(expectedMessage, ihrbException.getMessage()); + } + + @Test + void testExecute_typicalCase_createsSuccessfully() { + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + request.setInstructorComments("My road leads into the desert. I can see it."); + CreateAccountRequestAction action = getAction(request); + JsonResult result = getJsonResult(action); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertEquals("kwisatz.haderach@atreides.org", output.getEmail()); + assertEquals("Paul Atreides", output.getName()); + assertEquals("House Atreides", output.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, output.getStatus()); + assertEquals("My road leads into the desert. I can see it.", output.getComments()); + assertNull(output.getRegisteredAt()); + HibernateUtil.beginTransaction(); + AccountRequest accountRequest = logic.getAccountRequestByRegistrationKey(output.getRegistrationKey()); + HibernateUtil.commitTransaction(); + assertEquals("kwisatz.haderach@atreides.org", accountRequest.getEmail()); + assertEquals("Paul Atreides", accountRequest.getName()); + assertEquals("House Atreides", accountRequest.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, accountRequest.getStatus()); + assertEquals("My road leads into the desert. I can see it.", accountRequest.getComments()); + assertNull(accountRequest.getRegisteredAt()); + verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); + verifyNumberOfEmailsSent(2); + EmailWrapper sentAdminAlertEmail = mockEmailSender.getEmailsSent().get(0); + EmailWrapper sentAcknowledgementEmail = mockEmailSender.getEmailsSent().get(1); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, sentAdminAlertEmail.getType()); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, sentAcknowledgementEmail.getType()); + } + + @Test + void testExecute_leadingAndTrailingSpacesAndNullComments_createsSuccessfully() { AccountCreateRequest request = new AccountCreateRequest(); - request.setInstructorEmail("ring-bearer@fellowship.net"); - request.setInstructorName("Frodo Baggins"); - request.setInstructorInstitution("The Fellowship of the Ring"); + request.setInstructorEmail(" kwisatz.haderach@atreides.org "); + request.setInstructorName(" Paul Atreides "); + request.setInstructorInstitution(" House Atreides "); CreateAccountRequestAction action = getAction(request); JsonResult result = getJsonResult(action); - JoinLinkData output = (JoinLinkData) result.getOutput(); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertEquals("kwisatz.haderach@atreides.org", output.getEmail()); + assertEquals("Paul Atreides", output.getName()); + assertEquals("House Atreides", output.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, output.getStatus()); + assertNull(output.getComments()); + assertNull(output.getRegisteredAt()); HibernateUtil.beginTransaction(); - AccountRequest accountRequest = logic.getAccountRequest("ring-bearer@fellowship.net", "The Fellowship of the Ring"); + AccountRequest accountRequest = logic.getAccountRequestByRegistrationKey(output.getRegistrationKey()); HibernateUtil.commitTransaction(); - assertEquals("ring-bearer@fellowship.net", accountRequest.getEmail()); - assertEquals("Frodo Baggins", accountRequest.getName()); - assertEquals("The Fellowship of the Ring", accountRequest.getInstitute()); + assertEquals("kwisatz.haderach@atreides.org", accountRequest.getEmail()); + assertEquals("Paul Atreides", accountRequest.getName()); + assertEquals("House Atreides", accountRequest.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, accountRequest.getStatus()); + assertNull(accountRequest.getComments()); assertNull(accountRequest.getRegisteredAt()); - assertEquals(accountRequest.getRegistrationUrl(), output.getJoinLink()); - verifyNumberOfEmailsSent(1); verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); - EmailWrapper emailSent = mockEmailSender.getEmailsSent().get(0); - assertEquals(String.format(EmailType.NEW_INSTRUCTOR_ACCOUNT.getSubject(), "Frodo Baggins"), - emailSent.getSubject()); - assertEquals("ring-bearer@fellowship.net", emailSent.getRecipient()); - assertTrue(emailSent.getContent().contains(output.getJoinLink())); + verifyNumberOfEmailsSent(2); + EmailWrapper sentAdminAlertEmail = mockEmailSender.getEmailsSent().get(0); + EmailWrapper sentAcknowledgementEmail = mockEmailSender.getEmailsSent().get(1); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, sentAdminAlertEmail.getType()); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, sentAcknowledgementEmail.getType()); + } + + @Test + void testExecute_accountRequestWithSameEmailAddressAndInstituteAlreadyExists_createsSuccessfully() + throws InvalidParametersException { + HibernateUtil.beginTransaction(); + AccountRequest existingAccountRequest = logic.createAccountRequest("Paul Atreides", + "kwisatz.haderach@atreides.org", + "House Atreides", AccountRequestStatus.PENDING, "My road leads into the desert. I can see it."); + HibernateUtil.commitTransaction(); + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + request.setInstructorComments("My road leads into the desert. I can see it."); + CreateAccountRequestAction action = getAction(request); + JsonResult result = getJsonResult(action); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertEquals("kwisatz.haderach@atreides.org", output.getEmail()); + assertEquals("Paul Atreides", output.getName()); + assertEquals("House Atreides", output.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, output.getStatus()); + assertEquals("My road leads into the desert. I can see it.", output.getComments()); + assertNull(output.getRegisteredAt()); + assertNotEquals(output.getRegistrationKey(), existingAccountRequest.getRegistrationKey()); + HibernateUtil.beginTransaction(); + AccountRequest accountRequest = logic.getAccountRequestByRegistrationKey(output.getRegistrationKey()); + HibernateUtil.commitTransaction(); + assertEquals("kwisatz.haderach@atreides.org", accountRequest.getEmail()); + assertEquals("Paul Atreides", accountRequest.getName()); + assertEquals("House Atreides", accountRequest.getInstitute()); + assertEquals(AccountRequestStatus.PENDING, accountRequest.getStatus()); + assertEquals("My road leads into the desert. I can see it.", accountRequest.getComments()); + assertNull(accountRequest.getRegisteredAt()); + verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); + verifyNumberOfEmailsSent(2); + EmailWrapper sentAdminAlertEmail = mockEmailSender.getEmailsSent().get(0); + EmailWrapper sentAcknowledgementEmail = mockEmailSender.getEmailsSent().get(1); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, sentAdminAlertEmail.getType()); + assertEquals(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, sentAcknowledgementEmail.getType()); + } + + @Test + void testExecute_typicalCaseAsAdmin_noEmailsSent() { + loginAsAdminWithTransaction(); + AccountCreateRequest request = new AccountCreateRequest(); + request.setInstructorEmail("kwisatz.haderach@atreides.org"); + request.setInstructorName("Paul Atreides"); + request.setInstructorInstitution("House Atreides"); + request.setInstructorComments("My road leads into the desert. I can see it."); + CreateAccountRequestAction action = getAction(request); + JsonResult result = getJsonResult(action); + AccountRequestData output = (AccountRequestData) result.getOutput(); + assertNull(output.getRegisteredAt()); + verifyNoEmailsSent(); + logoutUser(); } @Override + @Test protected void testAccessControl() throws Exception { - // This is not tested due to upcoming changes in behaviour. + verifyAccessibleWithoutLogin(); } + @Override + @AfterMethod + protected void tearDown() { + HibernateUtil.beginTransaction(); + List accountRequests = logic.getPendingAccountRequests(); + for (AccountRequest ar : accountRequests) { + logic.deleteAccountRequest(ar.getId()); + } + accountRequests = logic.getPendingAccountRequests(); + HibernateUtil.commitTransaction(); + assert accountRequests.isEmpty(); + } } diff --git a/src/it/java/teammates/it/ui/webapi/GetAccountRequestsActionIT.java b/src/it/java/teammates/it/ui/webapi/GetAccountRequestsActionIT.java new file mode 100644 index 00000000000..feecca65747 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/GetAccountRequestsActionIT.java @@ -0,0 +1,109 @@ +package teammates.it.ui.webapi; + +import java.util.List; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.AccountRequest; +import teammates.storage.sqlentity.Course; +import teammates.ui.output.AccountRequestData; +import teammates.ui.output.AccountRequestsData; +import teammates.ui.webapi.GetAccountRequestsAction; +import teammates.ui.webapi.JsonResult; + +/** + * SUT: {@link GetAccountRequestsAction}. + */ +public class GetAccountRequestsActionIT extends BaseActionIT { + private final String[] validParams = { Const.ParamsNames.ACCOUNT_REQUEST_STATUS, "pending" }; + + @Override + @BeforeMethod + protected void setUp() throws Exception { + super.setUp(); + persistDataBundle(typicalBundle); + HibernateUtil.flushSession(); + } + + @Override + protected String getActionUri() { + return Const.ResourceURIs.ACCOUNT_REQUESTS; + } + + @Override + protected String getRequestMethod() { + return GET; + } + + @Override + @Test + public void testExecute() { + ______TS("accountrequeststatus param is null"); + + verifyHttpParameterFailure(); + verifyHttpParameterFailure(Const.ParamsNames.ACCOUNT_REQUEST_STATUS, "pendin"); + + ______TS("No pending account requests initially"); + + GetAccountRequestsAction action = getAction(this.validParams); + JsonResult result = getJsonResult(action); + AccountRequestsData data = (AccountRequestsData) result.getOutput(); + List arData = data.getAccountRequests(); + + assertEquals(0, arData.size()); + + ______TS("1 pending account request, case insensitive match for status request param"); + + AccountRequest accountRequest1 = typicalBundle.accountRequests.get("instructor1"); + accountRequest1.setStatus(AccountRequestStatus.PENDING); + + String[] params = { Const.ParamsNames.ACCOUNT_REQUEST_STATUS, "PendinG" }; + action = getAction(params); + result = getJsonResult(action); + data = (AccountRequestsData) result.getOutput(); + arData = data.getAccountRequests(); + + assertEquals(1, arData.size()); + + ______TS("Get 2 pending account requests, ignore 1 approved account request"); + AccountRequest approvedAccountRequest1 = typicalBundle.accountRequests.get("instructor2"); + approvedAccountRequest1.setStatus(AccountRequestStatus.APPROVED); + + accountRequest1 = typicalBundle.accountRequests.get("instructor1"); + AccountRequest accountRequest2 = typicalBundle.accountRequests.get("instructor1OfCourse2"); + accountRequest1.setStatus(AccountRequestStatus.PENDING); + accountRequest2.setStatus(AccountRequestStatus.PENDING); + + action = getAction(this.validParams); + result = getJsonResult(action); + data = (AccountRequestsData) result.getOutput(); + arData = data.getAccountRequests(); + + assertEquals(2, arData.size()); + + // account request 1 (with the most recent created_at) + assertEquals(arData.get(1).getEmail(), accountRequest1.getEmail()); + assertEquals(arData.get(1).getInstitute(), accountRequest1.getInstitute()); + assertEquals(arData.get(1).getName(), accountRequest1.getName()); + assertEquals(arData.get(1).getRegistrationKey(), accountRequest1.getRegistrationKey()); + + // account request 2 + assertEquals(arData.get(0).getEmail(), accountRequest2.getEmail()); + assertEquals(arData.get(0).getInstitute(), accountRequest2.getInstitute()); + assertEquals(arData.get(0).getName(), accountRequest2.getName()); + assertEquals(arData.get(0).getRegistrationKey(), accountRequest2.getRegistrationKey()); + } + + @Override + @Test + public void testAccessControl() throws InvalidParametersException, EntityAlreadyExistsException { + Course course = typicalBundle.courses.get("course1"); + verifyOnlyAdminCanAccess(course); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java b/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java index 4f024306105..2c322b23394 100644 --- a/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java +++ b/src/it/java/teammates/it/ui/webapi/GetCourseJoinStatusActionIT.java @@ -5,6 +5,7 @@ import teammates.common.util.Const; import teammates.common.util.HibernateUtil; +import teammates.storage.sqlentity.AccountRequest; import teammates.ui.output.JoinStatus; import teammates.ui.webapi.GetCourseJoinStatusAction; import teammates.ui.webapi.JsonResult; @@ -131,8 +132,8 @@ protected void testExecute() { ______TS("Normal case: account request not used, instructor has not joined course"); - String accountRequestNotUsedKey = logic.getAccountRequest("unregisteredinstructor1@gmail.tmt", - "TEAMMATES Test Institute 1").getRegistrationKey(); + AccountRequest unregisteredInstructor1AccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + String accountRequestNotUsedKey = unregisteredInstructor1AccountRequest.getRegistrationKey(); params = new String[] { Const.ParamsNames.REGKEY, accountRequestNotUsedKey, @@ -148,8 +149,8 @@ protected void testExecute() { ______TS("Normal case: account request already used, instructor has joined course"); - String accountRequestUsedKey = - logic.getAccountRequest("instr1@teammates.tmt", "TEAMMATES Test Institute 1").getRegistrationKey(); + AccountRequest instructor1AccountRequest = typicalBundle.accountRequests.get("instructor1"); + String accountRequestUsedKey = instructor1AccountRequest.getRegistrationKey(); params = new String[] { Const.ParamsNames.REGKEY, accountRequestUsedKey, diff --git a/src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java b/src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java new file mode 100644 index 00000000000..78f25dd89e8 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java @@ -0,0 +1,229 @@ +package teammates.it.ui.webapi; + +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Config; +import teammates.common.util.Const; +import teammates.common.util.EmailType; +import teammates.common.util.EmailWrapper; +import teammates.common.util.HibernateUtil; +import teammates.common.util.SanitizationHelper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestRejectionRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; +import teammates.ui.webapi.EntityNotFoundException; +import teammates.ui.webapi.InvalidHttpParameterException; +import teammates.ui.webapi.InvalidOperationException; +import teammates.ui.webapi.JsonResult; +import teammates.ui.webapi.RejectAccountRequestAction; + +/** + * SUT: {@link RejectAccountRequestAction}. + */ +public class RejectAccountRequestActionIT extends BaseActionIT { + + private static final String TYPICAL_TITLE = "We are Unable to Create an Account for you"; + private static final String TYPICAL_BODY = new StringBuilder() + .append("

Hi, Example

\n") + .append("

Thanks for your interest in using TEAMMATES. ") + .append("We are unable to create a TEAMMATES instructor account for you.

\n\n") + .append("

\n") + .append(" Reason: The email address you provided ") + .append("is not an 'official' email address provided by your institution.
\n") + .append(" Remedy: ") + .append("Please re-submit an account request with your 'official' institution email address.\n") + .append("

\n\n") + .append("

If you need further clarification or would like to appeal this decision, ") + .append("please feel free to contact us at teammates@comp.nus.edu.sg.

\n") + .append("

Regards,
TEAMMATES Team.

\n") + .toString(); + + @Override + @BeforeMethod + protected void setUp() throws Exception { + // no need to call super.setUp() because the action handles its own transactions + } + + @Override + protected String getActionUri() { + return Const.ResourceURIs.ACCOUNT_REQUEST_REJECTION; + } + + @Override + protected String getRequestMethod() { + return POST; + } + + @Override + public void testExecute() throws Exception { + // See individual test methods below + } + + @Test + protected void testExecute_withReasonTitleAndBody_shouldRejectWithEmail() + throws InvalidOperationException, InvalidHttpRequestBodyException, InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + AccountRequestStatus.PENDING, bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(TYPICAL_TITLE, TYPICAL_BODY); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + RejectAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(200, result.getStatusCode()); + + AccountRequestData data = (AccountRequestData) result.getOutput(); + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(AccountRequestStatus.REJECTED, data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + + verifyNumberOfEmailsSent(1); + EmailWrapper sentEmail = mockEmailSender.getEmailsSent().get(0); + assertEquals(EmailType.ACCOUNT_REQUEST_REJECTION, sentEmail.getType()); + assertEquals(Config.SUPPORT_EMAIL, sentEmail.getBcc()); + assertEquals(accountRequest.getEmail(), sentEmail.getRecipient()); + assertEquals(SanitizationHelper.sanitizeForRichText(TYPICAL_BODY), sentEmail.getContent()); + assertEquals("TEAMMATES: " + TYPICAL_TITLE, sentEmail.getSubject()); + } + + @Test + protected void testExecute_withoutReasonTitleAndBody_shouldRejectWithoutEmail() + throws InvalidOperationException, InvalidHttpRequestBodyException, InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + AccountRequestStatus.PENDING, bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + RejectAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(200, result.getStatusCode()); + + AccountRequestData data = (AccountRequestData) result.getOutput(); + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(AccountRequestStatus.REJECTED, data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_withReasonBodyButNoTitle_shouldThrow() throws InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + bundleAccountRequest.getStatus(), bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, TYPICAL_BODY); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + InvalidHttpRequestBodyException ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("Both reason body and title need to be null to reject silently", ihrbe.getMessage()); + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_withReasonTitleButNoBody_shouldThrow() throws InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + bundleAccountRequest.getStatus(), bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(TYPICAL_TITLE, null); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + InvalidHttpRequestBodyException ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("Both reason body and title need to be null to reject silently", ihrbe.getMessage()); + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_alreadyRejected_shouldNotSendEmail() + throws InvalidOperationException, InvalidHttpRequestBodyException, InvalidParametersException { + AccountRequest bundleAccountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction(bundleAccountRequest.getName(), + bundleAccountRequest.getEmail(), bundleAccountRequest.getInstitute(), + AccountRequestStatus.REJECTED, bundleAccountRequest.getComments()); + UUID id = accountRequest.getId(); + + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(TYPICAL_TITLE, TYPICAL_BODY); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + RejectAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(result.getStatusCode(), 200); + + AccountRequestData data = (AccountRequestData) result.getOutput(); + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(accountRequest.getStatus(), data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_invalidUuid_shouldThrow() throws InvalidParametersException { + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, "invalid"}; + + InvalidHttpParameterException ihpe = verifyHttpParameterFailure(requestBody, params); + assertEquals("Expected UUID value for id parameter, but found: [invalid]", ihpe.getMessage()); + verifyNoEmailsSent(); + } + + @Test + protected void testExecute_accountRequestNotFound_shouldThrow() { + AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null); + String uuid = UUID.randomUUID().toString(); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, uuid}; + + EntityNotFoundException enfe = verifyEntityNotFound(requestBody, params); + assertEquals(String.format("Account request with id = %s not found", uuid), enfe.getMessage()); + verifyNoEmailsSent(); + } + + @Override + @Test + protected void testAccessControl() throws InvalidParametersException, EntityAlreadyExistsException { + verifyOnlyAdminCanAccessWithTransaction(); + } + + @Override + @AfterMethod + protected void tearDown() { + HibernateUtil.beginTransaction(); + List accountRequests = logic.getAllAccountRequests(); + for (AccountRequest ar : accountRequests) { + logic.deleteAccountRequest(ar.getId()); + } + HibernateUtil.commitTransaction(); + } +} diff --git a/src/it/java/teammates/it/ui/webapi/UpdateAccountRequestActionIT.java b/src/it/java/teammates/it/ui/webapi/UpdateAccountRequestActionIT.java new file mode 100644 index 00000000000..f5932deaf99 --- /dev/null +++ b/src/it/java/teammates/it/ui/webapi/UpdateAccountRequestActionIT.java @@ -0,0 +1,262 @@ +package teammates.it.ui.webapi; + +import java.util.List; +import java.util.UUID; + +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.FieldValidator; +import teammates.common.util.HibernateUtil; +import teammates.common.util.StringHelperExtension; +import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestUpdateRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; +import teammates.ui.webapi.EntityNotFoundException; +import teammates.ui.webapi.InvalidHttpParameterException; +import teammates.ui.webapi.InvalidOperationException; +import teammates.ui.webapi.JsonResult; +import teammates.ui.webapi.UpdateAccountRequestAction; + +/** + * SUT: {@link UpdateAccountRequestAction}. + */ +public class UpdateAccountRequestActionIT extends BaseActionIT { + + @Override + @BeforeMethod + protected void setUp() throws Exception { + // no need to call super.setUp() because the action handles its own transactions + } + + @Override + protected String getActionUri() { + return Const.ResourceURIs.ACCOUNT_REQUEST; + } + + @Override + protected String getRequestMethod() { + return PUT; + } + + @Override + @Test + public void testExecute() throws Exception { + ______TS("edit fields of an account request"); + AccountRequest accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = accountRequest.getId(); + String name = "newName"; + String email = "newEmail@email.com"; + String institute = "newInstitute"; + String comments = "newComments"; + AccountRequestStatus status = accountRequest.getStatus(); + + AccountRequestUpdateRequest requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + UpdateAccountRequestAction action = getAction(requestBody, params); + JsonResult result = action.execute(); + + assertEquals(result.getStatusCode(), 200); + AccountRequestData data = (AccountRequestData) result.getOutput(); + + assertEquals(name, data.getName()); + assertEquals(email, data.getEmail()); + assertEquals(institute, data.getInstitute()); + assertEquals(status, data.getStatus()); + assertEquals(comments, data.getComments()); + verifyNoEmailsSent(); + + ______TS("approve a pending account request"); + accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + requestBody = new AccountRequestUpdateRequest(accountRequest.getName(), accountRequest.getEmail(), + accountRequest.getInstitute(), AccountRequestStatus.APPROVED, accountRequest.getComments()); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + action = getAction(requestBody, params); + result = getJsonResult(action, 200); + data = (AccountRequestData) result.getOutput(); + + assertEquals(accountRequest.getName(), data.getName()); + assertEquals(accountRequest.getEmail(), data.getEmail()); + assertEquals(accountRequest.getInstitute(), data.getInstitute()); + assertEquals(AccountRequestStatus.APPROVED, data.getStatus()); + assertEquals(accountRequest.getComments(), data.getComments()); + verifyNumberOfEmailsSent(1); + + ______TS("already registered account request has no email sent when approved"); + accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.REGISTERED, "comments"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, AccountRequestStatus.APPROVED, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + + action = getAction(requestBody, params); + result = getJsonResult(action, 200); + data = (AccountRequestData) result.getOutput(); + + assertEquals(name, data.getName()); + assertEquals(email, data.getEmail()); + assertEquals(institute, data.getInstitute()); + assertEquals(AccountRequestStatus.REGISTERED, data.getStatus()); + assertEquals(comments, data.getComments()); + verifyNumberOfEmailsSent(0); + + ______TS("email with existing account throws exception"); + Account account = logic.createAccountWithTransaction(getTypicalAccount()); + accountRequest = logic.createAccountRequestWithTransaction("name", account.getEmail(), + "institute", AccountRequestStatus.PENDING, "comments"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, AccountRequestStatus.APPROVED, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + + InvalidOperationException ipe = verifyInvalidOperation(requestBody, params); + + assertEquals(String.format("An account with email %s already exists. " + + "Please reject or delete the account request instead.", account.getEmail()), ipe.getMessage()); + + ______TS("non-existent but valid uuid"); + requestBody = new AccountRequestUpdateRequest("name", "email", + "institute", AccountRequestStatus.PENDING, "comments"); + String validUuid = UUID.randomUUID().toString(); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, validUuid}; + + EntityNotFoundException enfe = verifyEntityNotFound(requestBody, params); + + assertEquals(String.format("Account request with id = %s not found", validUuid), enfe.getMessage()); + + ______TS("invalid uuid"); + requestBody = new AccountRequestUpdateRequest("name", "email", + "institute", AccountRequestStatus.PENDING, "comments"); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, "invalid"}; + + InvalidHttpParameterException ihpe = verifyHttpParameterFailure(requestBody, params); + + assertEquals("Expected UUID value for id parameter, but found: [invalid]", ihpe.getMessage()); + + ______TS("invalid email"); + accountRequest = logic.createAccountRequestWithTransaction("name", "email@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + id = accountRequest.getId(); + email = "newEmail"; + status = accountRequest.getStatus(); + + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + InvalidHttpRequestBodyException ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals(getPopulatedErrorMessage(FieldValidator.EMAIL_ERROR_MESSAGE, email, + FieldValidator.EMAIL_FIELD_NAME, FieldValidator.REASON_INCORRECT_FORMAT, FieldValidator.EMAIL_MAX_LENGTH), + ihrbe.getMessage()); + + ______TS("invalid name alphanumeric"); + name = "@$@#$#@#@$#@$"; + email = "newEmail@email.com"; + + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals(getPopulatedErrorMessage(FieldValidator.INVALID_NAME_ERROR_MESSAGE, name, + FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.REASON_START_WITH_NON_ALPHANUMERIC_CHAR), + ihrbe.getMessage()); + + ______TS("invalid name too long"); + name = StringHelperExtension.generateStringOfLength(FieldValidator.PERSON_NAME_MAX_LENGTH + 1); + + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals(getPopulatedErrorMessage(FieldValidator.SIZE_CAPPED_NON_EMPTY_STRING_ERROR_MESSAGE, name, + FieldValidator.PERSON_NAME_FIELD_NAME, FieldValidator.REASON_TOO_LONG, + FieldValidator.PERSON_NAME_MAX_LENGTH), ihrbe.getMessage()); + + ______TS("null email value"); + name = "newName"; + + requestBody = new AccountRequestUpdateRequest(name, null, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("email cannot be null", ihrbe.getMessage()); + + ______TS("null name value"); + requestBody = new AccountRequestUpdateRequest(null, email, institute, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("name cannot be null", ihrbe.getMessage()); + + ______TS("null status value"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, null, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("status cannot be null", ihrbe.getMessage()); + + ______TS("null institute value"); + requestBody = new AccountRequestUpdateRequest(name, email, null, status, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + ihrbe = verifyHttpRequestBodyFailure(requestBody, params); + + assertEquals("institute cannot be null", ihrbe.getMessage()); + + ______TS("allow null comments in request"); + requestBody = new AccountRequestUpdateRequest(name, email, institute, status, null); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()}; + + action = getAction(requestBody, params); + result = getJsonResult(action, 200); + data = (AccountRequestData) result.getOutput(); + + assertEquals(name, data.getName()); + assertEquals(email, data.getEmail()); + assertEquals(institute, data.getInstitute()); + assertEquals(null, data.getComments()); + + ______TS("email with approved account request throws exception"); + logic.createAccountRequestWithTransaction("test", "test@email.com", + "institute", AccountRequestStatus.APPROVED, "comments"); + accountRequest = logic.createAccountRequestWithTransaction("test", "test@email.com", + "institute", AccountRequestStatus.PENDING, "comments"); + requestBody = new AccountRequestUpdateRequest(accountRequest.getName(), accountRequest.getEmail(), + accountRequest.getInstitute(), AccountRequestStatus.APPROVED, comments); + params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, accountRequest.getId().toString()}; + + ipe = verifyInvalidOperation(requestBody, params); + + assertEquals(String.format("An account request with email %s has already been approved. " + + "Please reject or delete the account request instead.", accountRequest.getEmail()), ipe.getMessage()); + } + + @Override + @Test + protected void testAccessControl() throws InvalidParametersException, EntityAlreadyExistsException { + verifyOnlyAdminCanAccessWithTransaction(); + } + + @Override + @AfterMethod + protected void tearDown() { + HibernateUtil.beginTransaction(); + List accountRequests = logic.getAllAccountRequests(); + for (AccountRequest ar : accountRequests) { + logic.deleteAccountRequest(ar.getId()); + } + HibernateUtil.commitTransaction(); + } +} diff --git a/src/it/resources/data/DataBundleLogicIT.json b/src/it/resources/data/DataBundleLogicIT.json index 49e7cdec993..371a87c963b 100644 --- a/src/it/resources/data/DataBundleLogicIT.json +++ b/src/it/resources/data/DataBundleLogicIT.json @@ -19,6 +19,8 @@ "name": "Instructor 1", "email": "instr1@teammates.tmt", "institute": "TEAMMATES Test Institute 1", + "status": "REGISTERED", + "comments": "These are some comments.", "registeredAt": "2015-02-14T00:00:00Z" } }, diff --git a/src/it/resources/data/typicalDataBundle.json b/src/it/resources/data/typicalDataBundle.json index 2372b2fa35f..4c97cca8512 100644 --- a/src/it/resources/data/typicalDataBundle.json +++ b/src/it/resources/data/typicalDataBundle.json @@ -73,14 +73,17 @@ "name": "Instructor 1", "email": "instr1@teammates.tmt", "institute": "TEAMMATES Test Institute 1", - "registeredAt": "2010-02-14T00:00:00Z" + "registeredAt": "2010-02-14T00:00:00Z", + "createdAt": "2011-02-01T00:00:00Z" }, "instructor2": { "id": "00000000-0000-4000-8000-000000000102", "name": "Instructor 2", "email": "instr2@teammates.tmt", "institute": "TEAMMATES Test Institute 1", - "registeredAt": "2015-02-14T00:00:00Z" + "registeredAt": "2015-02-14T00:00:00Z", + "comments": "Comments for account request from instructor2", + "status": "REGISTERED" }, "instructor3": { "name": "Instructor 3 of CourseNoRegister", diff --git a/src/main/java/teammates/common/datatransfer/AccountRequestStatus.java b/src/main/java/teammates/common/datatransfer/AccountRequestStatus.java new file mode 100644 index 00000000000..db80e7cb830 --- /dev/null +++ b/src/main/java/teammates/common/datatransfer/AccountRequestStatus.java @@ -0,0 +1,27 @@ +package teammates.common.datatransfer; + +/** + * The status of an account request. + */ +public enum AccountRequestStatus { + + /** + * The account request has not yet been processed by the admin. + */ + PENDING, + + /** + * The account request has been rejected by the admin. + */ + REJECTED, + + /** + * The account request has been approved by the admin but the instructor has not created an account yet. + */ + APPROVED, + + /** + * The account request has been approved by the admin and the instructor has created an account. + */ + REGISTERED +} diff --git a/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java b/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java index 70f97471d6f..95550962ed5 100644 --- a/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java +++ b/src/main/java/teammates/common/datatransfer/attributes/AccountRequestAttributes.java @@ -15,7 +15,7 @@ * The data transfer object for {@link AccountRequest} entities. */ public final class AccountRequestAttributes extends EntityAttributes { - + private String id; private String email; private String name; private String institute; @@ -38,7 +38,7 @@ private AccountRequestAttributes(String email, String institute, String name) { public static AccountRequestAttributes valueOf(AccountRequest accountRequest) { AccountRequestAttributes accountRequestAttributes = new AccountRequestAttributes(accountRequest.getEmail(), accountRequest.getInstitute(), accountRequest.getName()); - + accountRequestAttributes.id = accountRequest.getId(); accountRequestAttributes.registrationKey = accountRequest.getRegistrationKey(); accountRequestAttributes.registeredAt = accountRequest.getRegisteredAt(); accountRequestAttributes.createdAt = accountRequest.getCreatedAt(); @@ -53,6 +53,10 @@ public static Builder builder(String email, String institute, String name) { return new Builder(email, institute, name); } + public String getId() { + return id; + } + public String getRegistrationKey() { return registrationKey; } diff --git a/src/main/java/teammates/common/util/Const.java b/src/main/java/teammates/common/util/Const.java index b24d0ded648..ee741c14c1f 100644 --- a/src/main/java/teammates/common/util/Const.java +++ b/src/main/java/teammates/common/util/Const.java @@ -45,6 +45,8 @@ public final class Const { public static final String MISSING_RESPONSE_TEXT = "No Response"; + public static final String ACCOUNT_REQUEST_NOT_FOUND = "Account request with id = %s not found"; + // These constants are used as variable values to mean that the variable is in a 'special' state. public static final int INT_UNINITIALIZED = -9999; @@ -123,6 +125,9 @@ public static class ParamsNames { public static final String IS_CREATING_ACCOUNT = "iscreatingaccount"; public static final String IS_INSTRUCTOR = "isinstructor"; + public static final String ACCOUNT_REQUEST_ID = "id"; + public static final String ACCOUNT_REQUEST_STATUS = "status"; + public static final String FEEDBACK_SESSION_NAME = "fsname"; public static final String FEEDBACK_SESSION_STARTTIME = "starttime"; public static final String FEEDBACK_SESSION_ENDTIME = "endtime"; @@ -313,6 +318,8 @@ public static class WebPageURIs { public static final String SESSION_RESULTS_PAGE = URI_PREFIX + "/sessions/result"; public static final String SESSION_SUBMISSION_PAGE = URI_PREFIX + "/sessions/submission"; public static final String SESSIONS_LINK_RECOVERY_PAGE = FRONT_PAGE + "/help/session-links-recovery"; + + public static final String ACCOUNT_REQUEST_PAGE = FRONT_PAGE + "/request"; } /** @@ -332,7 +339,9 @@ public static class ResourceURIs { public static final String ACCOUNT = URI_PREFIX + "/account"; public static final String ACCOUNT_RESET = URI_PREFIX + "/account/reset"; public static final String ACCOUNT_REQUEST = URI_PREFIX + "/account/request"; + public static final String ACCOUNT_REQUESTS = URI_PREFIX + "/account/requests"; public static final String ACCOUNT_REQUEST_RESET = ACCOUNT_REQUEST + "/reset"; + public static final String ACCOUNT_REQUEST_REJECTION = ACCOUNT_REQUEST + "/rejection"; public static final String ACCOUNTS = URI_PREFIX + "/accounts"; public static final String RESPONSE_COMMENT = URI_PREFIX + "/responsecomment"; public static final String COURSE = URI_PREFIX + "/course"; diff --git a/src/main/java/teammates/common/util/EmailType.java b/src/main/java/teammates/common/util/EmailType.java index af649debb7b..a42280ba7f0 100644 --- a/src/main/java/teammates/common/util/EmailType.java +++ b/src/main/java/teammates/common/util/EmailType.java @@ -23,6 +23,9 @@ public enum EmailType { NEW_INSTRUCTOR_ACCOUNT("TEAMMATES: Welcome to TEAMMATES! %s"), STUDENT_COURSE_JOIN("TEAMMATES: Invitation to join course [%s][Course ID: %s]"), STUDENT_COURSE_REJOIN_AFTER_GOOGLE_ID_RESET("TEAMMATES: Your account has been reset for course [%s][Course ID: %s]"), + NEW_ACCOUNT_REQUEST_ADMIN_ALERT("TEAMMATES (Action Needed): New Account Request Received"), + NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT("TEAMMATES: Acknowledgement of Instructor Account Request"), + ACCOUNT_REQUEST_REJECTION("TEAMMATES: %s"), INSTRUCTOR_COURSE_JOIN("TEAMMATES: Invitation to join course as an instructor [%s][Course ID: %s]"), INSTRUCTOR_COURSE_REJOIN_AFTER_GOOGLE_ID_RESET("TEAMMATES: Your account has been reset for course [%s][Course ID: %s]"), USER_COURSE_REGISTER("TEAMMATES: Registered for Course [%s][Course ID: %s]"), diff --git a/src/main/java/teammates/common/util/FieldValidator.java b/src/main/java/teammates/common/util/FieldValidator.java index 10b1b34a682..1e46adbff34 100644 --- a/src/main/java/teammates/common/util/FieldValidator.java +++ b/src/main/java/teammates/common/util/FieldValidator.java @@ -46,7 +46,7 @@ public final class FieldValidator { public static final int SECTION_NAME_MAX_LENGTH = 60; public static final String INSTITUTE_NAME_FIELD_NAME = "institute name"; - public static final int INSTITUTE_NAME_MAX_LENGTH = 64; + public static final int INSTITUTE_NAME_MAX_LENGTH = 128; // email-related public static final String EMAIL_FIELD_NAME = "email"; diff --git a/src/main/java/teammates/common/util/Templates.java b/src/main/java/teammates/common/util/Templates.java index 524c84bc2c6..1fea9a61405 100644 --- a/src/main/java/teammates/common/util/Templates.java +++ b/src/main/java/teammates/common/util/Templates.java @@ -32,6 +32,10 @@ public static String populateTemplate(String template, String... keyValuePairs) * Collection of templates of emails to be sent by the system. */ public static class EmailTemplates { + public static final String ADMIN_NEW_ACCOUNT_REQUEST_ALERT = + FileHelper.readResourceFile("adminEmailTemplate-newAccountRequestAlert.html"); + public static final String INSTRUCTOR_NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT = + FileHelper.readResourceFile("instructorEmailTemplate-newAccountRequestAcknowledgement.html"); public static final String USER_COURSE_JOIN = FileHelper.readResourceFile("userEmailTemplate-courseJoin.html"); public static final String USER_COURSE_REGISTER = diff --git a/src/main/java/teammates/logic/api/TaskQueuer.java b/src/main/java/teammates/logic/api/TaskQueuer.java index a3db7d7d359..2ea43cf850b 100644 --- a/src/main/java/teammates/logic/api/TaskQueuer.java +++ b/src/main/java/teammates/logic/api/TaskQueuer.java @@ -218,15 +218,13 @@ public void scheduleInstructorForSearchIndexing(String courseId, String email) { } /** - * Schedules for the search indexing of the account request identified by {@code email} and {@code institute}. + * Schedules for the search indexing of the account request identified by {@code id}. * - * @param email the email associated with the account request - * @param institute the institute associated with the account request + * @param id the id associated with the account request */ - public void scheduleAccountRequestForSearchIndexing(String email, String institute) { + public void scheduleAccountRequestForSearchIndexing(String id) { Map paramMap = new HashMap<>(); - paramMap.put(ParamsNames.INSTRUCTOR_EMAIL, email); - paramMap.put(ParamsNames.INSTRUCTOR_INSTITUTION, institute); + paramMap.put(ParamsNames.ACCOUNT_REQUEST_ID, id); addTask(TaskQueue.SEARCH_INDEXING_QUEUE_NAME, TaskQueue.ACCOUNT_REQUEST_SEARCH_INDEXING_WORKER_URL, paramMap, null); diff --git a/src/main/java/teammates/sqllogic/api/Logic.java b/src/main/java/teammates/sqllogic/api/Logic.java index f83ff914ec7..a70ea6b255d 100644 --- a/src/main/java/teammates/sqllogic/api/Logic.java +++ b/src/main/java/teammates/sqllogic/api/Logic.java @@ -8,6 +8,7 @@ import javax.annotation.Nullable; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.FeedbackQuestionRecipient; import teammates.common.datatransfer.FeedbackResultFetchType; import teammates.common.datatransfer.NotificationStyle; @@ -88,32 +89,41 @@ public static Logic inst() { * @throws InvalidParametersException if the account request details are invalid. * @throws EntityAlreadyExistsException if the account request already exists. */ - public AccountRequest createAccountRequest(String name, String email, String institute) - throws InvalidParametersException, EntityAlreadyExistsException { + public AccountRequest createAccountRequest(String name, String email, String institute, AccountRequestStatus status, + String comments) throws InvalidParametersException { - return accountRequestLogic.createAccountRequest(name, email, institute); + return accountRequestLogic.createAccountRequest(name, email, institute, status, comments); } /** - * Creates a or gets an account request. + * Gets the account request with the given {@code id}. * - * @return newly created account request. - * @throws InvalidParametersException if the account request details are invalid. - * @throws EntityAlreadyExistsException if the account request already exists. + * @return account request with the given {@code id}. */ - public AccountRequest createAccountRequestWithTransaction(String name, String email, String institute) - throws InvalidParametersException { + public AccountRequest getAccountRequest(UUID id) { + return accountRequestLogic.getAccountRequest(id); + } - return accountRequestLogic.createOrGetAccountRequestWithTransaction(name, email, institute); + /** + * Gets the account request with the given {@code id}. + * + * @return account request with the given {@code id}. + */ + public AccountRequest getAccountRequestWithTransaction(UUID id) { + return accountRequestLogic.getAccountRequestWithTransaction(id); } /** - * Gets the account request with the given email and institute. + * Creates a or gets an account request. * - * @return account request with the given email and institute. + * @return newly created account request. + * @throws InvalidParametersException if the account request details are invalid. + * @throws EntityAlreadyExistsException if the account request already exists. */ - public AccountRequest getAccountRequest(String email, String institute) { - return accountRequestLogic.getAccountRequest(email, institute); + public AccountRequest createAccountRequestWithTransaction(String name, String email, String institute, + AccountRequestStatus status, String comments) throws InvalidParametersException { + + return accountRequestLogic.createOrGetAccountRequestWithTransaction(name, email, institute, status, comments); } /** @@ -136,19 +146,29 @@ public AccountRequest updateAccountRequest(AccountRequest accountRequest) } /** - * Creates/Resets the account request with the given email and institute + * Updates the given account request. + * + * @return the updated account request. + */ + public AccountRequest updateAccountRequestWithTransaction(AccountRequest accountRequest) + throws InvalidParametersException, EntityDoesNotExistException { + return accountRequestLogic.updateAccountRequestWithTransaction(accountRequest); + } + + /** + * Creates/Resets the account request with the given id * such that it is not registered. * * @return account request that is unregistered with the - * email and institute. + * id. */ - public AccountRequest resetAccountRequest(String email, String institute) + public AccountRequest resetAccountRequest(UUID id) throws EntityDoesNotExistException, InvalidParametersException { - return accountRequestLogic.resetAccountRequest(email, institute); + return accountRequestLogic.resetAccountRequest(id); } /** - * Deletes account request by email and institute. + * Deletes account request by id. * *
    *
  • Fails silently if no such account request.
  • @@ -157,8 +177,29 @@ public AccountRequest resetAccountRequest(String email, String institute) *

    Preconditions:

    * All parameters are non-null. */ - public void deleteAccountRequest(String email, String institute) { - accountRequestLogic.deleteAccountRequest(email, institute); + public void deleteAccountRequest(UUID id) { + accountRequestLogic.deleteAccountRequest(id); + } + + /** + * Gets all pending account requests. + */ + public List getPendingAccountRequests() { + return accountRequestLogic.getPendingAccountRequests(); + } + + /** + * Gets all pending account requests. + */ + public List getAllAccountRequests() { + return accountRequestLogic.getAllAccountRequests(); + } + + /** + * Get a list of account requests associated with email provided. + */ + public List getApprovedAccountRequestsForEmailWithTransaction(String email) { + return accountRequestLogic.getApprovedAccountRequestsForEmailWithTransaction(email); } /** @@ -182,6 +223,13 @@ public List getAccountsForEmail(String email) { return accountsLogic.getAccountsForEmail(email); } + /** + * Get a list of accounts associated with email provided. + */ + public List getAccountsForEmailWithTransaction(String email) { + return accountsLogic.getAccountsForEmailWithTransaction(email); + } + /** * Creates an account. * @@ -194,6 +242,18 @@ public Account createAccount(Account account) return accountsLogic.createAccount(account); } + /** + * Creates an account. + * + * @return the created account + * @throws InvalidParametersException if the account is not valid + * @throws EntityAlreadyExistsException if the account already exists in the database. + */ + public Account createAccountWithTransaction(Account account) + throws InvalidParametersException, EntityAlreadyExistsException { + return accountsLogic.createAccountWithTransaction(account); + } + /** * Deletes account by googleId. * diff --git a/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java b/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java index 50add8e08ff..cc5fc4507e7 100644 --- a/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java +++ b/src/main/java/teammates/sqllogic/api/SqlEmailGenerator.java @@ -26,6 +26,7 @@ import teammates.sqllogic.core.FeedbackSessionsLogic; import teammates.sqllogic.core.UsersLogic; import teammates.storage.sqlentity.Account; +import teammates.storage.sqlentity.AccountRequest; import teammates.storage.sqlentity.Course; import teammates.storage.sqlentity.DeadlineExtension; import teammates.storage.sqlentity.FeedbackSession; @@ -971,6 +972,74 @@ public EmailWrapper generateInstructorCourseRejoinEmailAfterGoogleIdReset( return email; } + /** + * Generates the email to alert the admin of the new {@code accountRequest}. + */ + public EmailWrapper generateNewAccountRequestAdminAlertEmail(AccountRequest accountRequest) { + String name = accountRequest.getName(); + String institute = accountRequest.getInstitute(); + String emailAddress = accountRequest.getEmail(); + String comments = accountRequest.getComments(); + if (comments == null) { + comments = ""; + } + String adminAccountRequestsPageUrl = Config.getFrontEndAppUrl(Const.WebPageURIs.ADMIN_HOME_PAGE).toAbsoluteString(); + String[] templateKeyValuePairs = new String[] { + "${name}", name, + "${institute}", institute, + "${emailAddress}", emailAddress, + "${comments}", comments, + "${adminAccountRequestsPageUrl}", adminAccountRequestsPageUrl, + }; + String content = Templates.populateTemplate(EmailTemplates.ADMIN_NEW_ACCOUNT_REQUEST_ALERT, templateKeyValuePairs); + EmailWrapper email = getEmptyEmailAddressedToEmail(Config.SUPPORT_EMAIL); + email.setType(EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT); + email.setSubjectFromType(); + email.setContent(content); + return email; + } + + /** + * Generates the acknowledgement email to be sent to the person who submitted {@code accountRequest}. + */ + public EmailWrapper generateNewAccountRequestAcknowledgementEmail(AccountRequest accountRequest) { + String name = SanitizationHelper.sanitizeForHtml(accountRequest.getName()); + String institute = SanitizationHelper.sanitizeForHtml(accountRequest.getInstitute()); + String emailAddress = SanitizationHelper.sanitizeForHtml(accountRequest.getEmail()); + String comments = SanitizationHelper.sanitizeForHtml(accountRequest.getComments()); + if (comments == null) { + comments = ""; + } + String[] templateKeyValuePairs = new String[] { + "${name}", name, + "${institute}", institute, + "${emailAddress}", emailAddress, + "${comments}", comments, + "${supportEmail}", Config.SUPPORT_EMAIL, + }; + String content = Templates.populateTemplate( + EmailTemplates.INSTRUCTOR_NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, templateKeyValuePairs); + EmailWrapper email = getEmptyEmailAddressedToEmail(emailAddress); + email.setType(EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT); + email.setBcc(Config.SUPPORT_EMAIL); + email.setSubjectFromType(); + email.setContent(content); + return email; + } + + /** + * Generates the email to be sent to instructor when their account request has been rejected by admin. + */ + public EmailWrapper generateAccountRequestRejectionEmail(AccountRequest accountRequest, String title, String content) { + EmailWrapper email = getEmptyEmailAddressedToEmail(accountRequest.getEmail()); + email.setType(EmailType.ACCOUNT_REQUEST_REJECTION); + email.setBcc(Config.SUPPORT_EMAIL); + email.setSubjectFromType(SanitizationHelper.sanitizeTitle(title)); + email.setContent(SanitizationHelper.sanitizeForRichText(content)); + + return email; + } + /** * Generates the course registered email for the user with the given details in {@code course}. */ diff --git a/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java b/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java index f0797adf034..996e52abb0a 100644 --- a/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java +++ b/src/main/java/teammates/sqllogic/core/AccountRequestsLogic.java @@ -1,8 +1,9 @@ package teammates.sqllogic.core; import java.util.List; +import java.util.UUID; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.exception.SearchServiceException; @@ -52,27 +53,35 @@ public void putDocument(AccountRequest accountRequest) throws SearchServiceExcep /** * Creates an account request. */ - public AccountRequest createAccountRequest(AccountRequest accountRequest) - throws InvalidParametersException, EntityAlreadyExistsException { + public AccountRequest createAccountRequest(AccountRequest accountRequest) throws InvalidParametersException { return accountRequestDb.createAccountRequest(accountRequest); } /** * Creates an account request. */ - public AccountRequest createAccountRequest(String name, String email, String institute) - throws InvalidParametersException, EntityAlreadyExistsException { - AccountRequest toCreate = new AccountRequest(email, name, institute); + public AccountRequest createAccountRequest(String name, String email, String institute, AccountRequestStatus status, + String comments) throws InvalidParametersException { + AccountRequest toCreate = new AccountRequest(email, name, institute, status, comments); return accountRequestDb.createAccountRequest(toCreate); } /** - * Gets account request associated with the {@code email} and {@code institute}. + * Gets the account request associated with the {@code id}. */ - public AccountRequest getAccountRequest(String email, String institute) { + public AccountRequest getAccountRequest(UUID id) { + return accountRequestDb.getAccountRequest(id); + } - return accountRequestDb.getAccountRequest(email, institute); + /** + * Gets the account request associated with the {@code id}. + */ + public AccountRequest getAccountRequestWithTransaction(UUID id) { + HibernateUtil.beginTransaction(); + AccountRequest request = accountRequestDb.getAccountRequest(id); + HibernateUtil.commitTransaction(); + return request; } /** @@ -83,6 +92,27 @@ public AccountRequest updateAccountRequest(AccountRequest accountRequest) return accountRequestDb.updateAccountRequest(accountRequest); } + /** + * Updates an account request. + */ + @SuppressWarnings("PMD") + public AccountRequest updateAccountRequestWithTransaction(AccountRequest accountRequest) + throws InvalidParametersException, EntityDoesNotExistException { + + HibernateUtil.beginTransaction(); + AccountRequest updatedRequest; + + try { + updatedRequest = accountRequestDb.updateAccountRequest(accountRequest); + HibernateUtil.commitTransaction(); + } catch (InvalidParametersException ipe) { + HibernateUtil.rollbackTransaction(); + throw new InvalidParametersException(ipe.getMessage()); + } + + return updatedRequest; + } + /** * Gets account request associated with the {@code regkey}. */ @@ -91,15 +121,39 @@ public AccountRequest getAccountRequestByRegistrationKey(String regkey) { } /** - * Creates/resets the account request with the given email and institute such that it is not registered. + * Gets all pending account requests. + */ + public List getPendingAccountRequests() { + return accountRequestDb.getPendingAccountRequests(); + } + + /** + * Gets all account requests. + */ + public List getAllAccountRequests() { + return accountRequestDb.getAllAccountRequests(); + } + + /** + * Get a list of account requests associated with email provided. + */ + public List getApprovedAccountRequestsForEmailWithTransaction(String email) { + HibernateUtil.beginTransaction(); + List accountRequests = accountRequestDb.getApprovedAccountRequestsForEmail(email); + HibernateUtil.commitTransaction(); + return accountRequests; + } + + /** + * Creates/resets the account request with the given id such that it is not registered. */ - public AccountRequest resetAccountRequest(String email, String institute) + public AccountRequest resetAccountRequest(UUID id) throws EntityDoesNotExistException, InvalidParametersException { - AccountRequest accountRequest = accountRequestDb.getAccountRequest(email, institute); + AccountRequest accountRequest = accountRequestDb.getAccountRequest(id); if (accountRequest == null) { throw new EntityDoesNotExistException("Failed to reset since AccountRequest with " - + "the given email and institute cannot be found."); + + "the given id cannot be found."); } accountRequest.setRegisteredAt(null); @@ -107,13 +161,13 @@ public AccountRequest resetAccountRequest(String email, String institute) } /** - * Deletes account request associated with the {@code email} and {@code institute}. + * Deletes account request associated with the {@code id}. * - *

    Fails silently if no account requests with the given email and institute to delete can be found.

    + *

    Fails silently if no account requests with the given id to delete can be found.

    * */ - public void deleteAccountRequest(String email, String institute) { - AccountRequest toDelete = accountRequestDb.getAccountRequest(email, institute); + public void deleteAccountRequest(UUID id) { + AccountRequest toDelete = accountRequestDb.getAccountRequest(id); accountRequestDb.deleteAccountRequest(toDelete); } @@ -131,9 +185,10 @@ public List searchAccountRequestsInWholeSystem(String queryStrin /** * Creates an or gets an account request. */ - public AccountRequest createOrGetAccountRequestWithTransaction(String name, String email, String institute) + public AccountRequest createOrGetAccountRequestWithTransaction(String name, String email, String institute, + AccountRequestStatus status, String comments) throws InvalidParametersException { - AccountRequest toCreate = new AccountRequest(email, name, institute); + AccountRequest toCreate = new AccountRequest(email, name, institute, status, comments); HibernateUtil.beginTransaction(); AccountRequest accountRequest; try { @@ -142,10 +197,6 @@ public AccountRequest createOrGetAccountRequestWithTransaction(String name, Stri } catch (InvalidParametersException ipe) { HibernateUtil.rollbackTransaction(); throw new InvalidParametersException(ipe); - } catch (EntityAlreadyExistsException eaee) { - // Use existing account request - accountRequest = getAccountRequest(email, institute); - HibernateUtil.commitTransaction(); } return accountRequest; } diff --git a/src/main/java/teammates/sqllogic/core/AccountsLogic.java b/src/main/java/teammates/sqllogic/core/AccountsLogic.java index 74bc4af732b..49948080448 100644 --- a/src/main/java/teammates/sqllogic/core/AccountsLogic.java +++ b/src/main/java/teammates/sqllogic/core/AccountsLogic.java @@ -8,6 +8,7 @@ import teammates.common.exception.EntityAlreadyExistsException; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; +import teammates.common.util.HibernateUtil; import teammates.storage.sqlapi.AccountsDb; import teammates.storage.sqlentity.Account; import teammates.storage.sqlentity.Course; @@ -77,6 +78,19 @@ public List getAccountsForEmail(String email) { return accountsDb.getAccountsByEmail(email); } + /** + * Gets accounts associated with email. + */ + public List getAccountsForEmailWithTransaction(String email) { + assert email != null; + + HibernateUtil.beginTransaction(); + List accounts = accountsDb.getAccountsByEmail(email); + HibernateUtil.commitTransaction(); + + return accounts; + } + /** * Creates an account. * @@ -91,6 +105,25 @@ public Account createAccount(Account account) return accountsDb.createAccount(account); } + /** + * Creates an account. + * + * @return the created account + * @throws InvalidParametersException if the account is not valid + * @throws EntityAlreadyExistsException if the account already exists in the + * database. + */ + public Account createAccountWithTransaction(Account account) + throws InvalidParametersException, EntityAlreadyExistsException { + assert account != null; + + HibernateUtil.beginTransaction(); + Account createdAccount = accountsDb.createAccount(account); + HibernateUtil.commitTransaction(); + + return createdAccount; + } + /** * Deletes account associated with the {@code googleId}. * diff --git a/src/main/java/teammates/sqllogic/core/DataBundleLogic.java b/src/main/java/teammates/sqllogic/core/DataBundleLogic.java index 8ab165d31b7..0c7c9f66311 100644 --- a/src/main/java/teammates/sqllogic/core/DataBundleLogic.java +++ b/src/main/java/teammates/sqllogic/core/DataBundleLogic.java @@ -351,7 +351,7 @@ public void removeDataBundle(SqlDataBundle dataBundle) throws InvalidParametersE accountsLogic.deleteAccount(account.getGoogleId()); }); dataBundle.accountRequests.values().forEach(accountRequest -> { - accountRequestsLogic.deleteAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + accountRequestsLogic.deleteAccountRequest(accountRequest.getId()); }); } diff --git a/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java b/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java index a315cb18484..c0fc9c74b0b 100644 --- a/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java +++ b/src/main/java/teammates/storage/sqlapi/AccountRequestsDb.java @@ -1,6 +1,5 @@ package teammates.storage.sqlapi; -import static teammates.common.util.Const.ERROR_CREATE_ENTITY_ALREADY_EXISTS; import static teammates.common.util.Const.ERROR_UPDATE_NON_EXISTENT; import java.time.Instant; @@ -9,7 +8,7 @@ import java.util.List; import java.util.UUID; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.exception.SearchServiceException; @@ -46,36 +45,64 @@ public AccountRequestSearchManager getSearchManager() { /** * Creates an AccountRequest in the database. */ - public AccountRequest createAccountRequest(AccountRequest accountRequest) - throws InvalidParametersException, EntityAlreadyExistsException { + public AccountRequest createAccountRequest(AccountRequest accountRequest) throws InvalidParametersException { assert accountRequest != null; if (!accountRequest.isValid()) { throw new InvalidParametersException(accountRequest.getInvalidityInfo()); } - - // don't need to check registrationKey for uniqueness since it is generated using email + institute - if (getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()) != null) { - throw new EntityAlreadyExistsException( - String.format(ERROR_CREATE_ENTITY_ALREADY_EXISTS, accountRequest.toString())); - } - persist(accountRequest); return accountRequest; } /** - * Get AccountRequest by {@code email} and {@code institute} from database. + * Get AccountRequest by {@code id} from the database. + */ + public AccountRequest getAccountRequest(UUID id) { + assert id != null; + return HibernateUtil.get(AccountRequest.class, id); + } + + /** + * Get all Account Requests with {@code status} of 'pending'. */ - public AccountRequest getAccountRequest(String email, String institute) { + public List getPendingAccountRequests() { CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery(AccountRequest.class); Root root = cr.from(AccountRequest.class); - cr.select(root).where(cb.and(cb.equal( - root.get("email"), email), cb.equal(root.get("institute"), institute))); + cr.select(root) + .where(cb.equal(root.get("status"), AccountRequestStatus.PENDING)) + .orderBy(cb.desc(root.get("createdAt"))); TypedQuery query = HibernateUtil.createQuery(cr); - return query.getResultStream().findFirst().orElse(null); + return query.getResultList(); + } + + /** + * Get all Account Requests. + */ + public List getAllAccountRequests() { + CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(AccountRequest.class); + Root root = cr.from(AccountRequest.class); + cr.select(root); + + TypedQuery query = HibernateUtil.createQuery(cr); + return query.getResultList(); + } + + /** + * Get all Account Requests for a given {@code email}. + */ + public List getApprovedAccountRequestsForEmail(String email) { + CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder(); + CriteriaQuery cr = cb.createQuery(AccountRequest.class); + Root root = cr.from(AccountRequest.class); + cr.select(root).where(cb.and(cb.equal(root.get("email"), email), + cb.equal(root.get("status"), AccountRequestStatus.APPROVED))); + + TypedQuery query = HibernateUtil.createQuery(cr); + return query.getResultList(); } /** @@ -116,7 +143,7 @@ public AccountRequest updateAccountRequest(AccountRequest accountRequest) throw new InvalidParametersException(accountRequest.getInvalidityInfo()); } - if (getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()) == null) { + if (getAccountRequest(accountRequest.getId()) == null) { throw new EntityDoesNotExistException( String.format(ERROR_UPDATE_NON_EXISTENT, accountRequest.toString())); } @@ -140,10 +167,8 @@ public void deleteAccountRequest(AccountRequest accountRequest) { */ public void deleteDocumentByAccountRequestId(UUID accountRequestId) { if (getSearchManager() != null) { - // Solr saves the id with the prefix "java.util.UUID:", so we need to add it here to - // identify and delete the document from the index getSearchManager().deleteDocuments( - Collections.singletonList("java.util.UUID:" + accountRequestId.toString())); + Collections.singletonList(accountRequestId.toString())); } } diff --git a/src/main/java/teammates/storage/sqlentity/AccountRequest.java b/src/main/java/teammates/storage/sqlentity/AccountRequest.java index 2389fbf352d..97a65e6b468 100644 --- a/src/main/java/teammates/storage/sqlentity/AccountRequest.java +++ b/src/main/java/teammates/storage/sqlentity/AccountRequest.java @@ -9,13 +9,17 @@ import org.hibernate.annotations.UpdateTimestamp; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.util.Config; import teammates.common.util.Const; import teammates.common.util.FieldValidator; import teammates.common.util.SanitizationHelper; import teammates.common.util.StringHelper; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; @@ -27,7 +31,6 @@ @Table(name = "AccountRequests", uniqueConstraints = { @UniqueConstraint(name = "Unique registration key", columnNames = "registrationKey"), - @UniqueConstraint(name = "Unique name and institute", columnNames = {"email", "institute"}) }) public class AccountRequest extends BaseEntity { @Id @@ -41,6 +44,12 @@ public class AccountRequest extends BaseEntity { private String institute; + @Enumerated(EnumType.STRING) + private AccountRequestStatus status; + + @Column(columnDefinition = "TEXT") + private String comments; + private Instant registeredAt; @UpdateTimestamp @@ -50,11 +59,13 @@ protected AccountRequest() { // required by Hibernate } - public AccountRequest(String email, String name, String institute) { + public AccountRequest(String email, String name, String institute, AccountRequestStatus status, String comments) { this.setId(UUID.randomUUID()); this.setEmail(email); this.setName(name); this.setInstitute(institute); + this.setStatus(status); + this.setComments(comments); this.generateNewRegistrationKey(); this.setCreatedAt(Instant.now()); this.setRegisteredAt(null); @@ -129,6 +140,22 @@ public void setInstitute(String institute) { this.institute = SanitizationHelper.sanitizeTitle(institute); } + public AccountRequestStatus getStatus() { + return this.status; + } + + public void setStatus(AccountRequestStatus status) { + this.status = status; + } + + public String getComments() { + return this.comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + public Instant getRegisteredAt() { return this.registeredAt; } @@ -167,8 +194,8 @@ public int hashCode() { @Override public String toString() { return "AccountRequest [id=" + id + ", registrationKey=" + registrationKey + ", name=" + name + ", email=" - + email + ", institute=" + institute + ", registeredAt=" + registeredAt + ", createdAt=" + getCreatedAt() - + ", updatedAt=" + updatedAt + "]"; + + email + ", institute=" + institute + ", status=" + status + ", comments=" + comments + + ", registeredAt=" + registeredAt + ", createdAt=" + getCreatedAt() + ", updatedAt=" + updatedAt + "]"; } public String getRegistrationUrl() { diff --git a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java index 9fbaf38ef14..15b6dd02e37 100644 --- a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java +++ b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchDocument.java @@ -1,5 +1,6 @@ package teammates.storage.sqlsearch; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -19,14 +20,29 @@ class AccountRequestSearchDocument extends SearchDocument { Map getSearchableFields() { Map fields = new HashMap<>(); AccountRequest accountRequest = entity; - String[] searchableTexts = { - accountRequest.getName(), accountRequest.getEmail(), accountRequest.getInstitute(), - }; - fields.put("id", accountRequest.getId()); + ArrayList searchableTexts = new ArrayList<>(); + searchableTexts.add(accountRequest.getName()); + searchableTexts.add(accountRequest.getEmail()); + searchableTexts.add(accountRequest.getInstitute()); + + if (accountRequest.getComments() != null) { + searchableTexts.add(accountRequest.getComments()); + } + if (accountRequest.getStatus() != null) { + searchableTexts.add(accountRequest.getStatus().toString()); + } + + fields.put("id", accountRequest.getId().toString()); fields.put("_text_", String.join(" ", searchableTexts)); fields.put("email", accountRequest.getEmail()); fields.put("institute", accountRequest.getInstitute()); + if (accountRequest.getComments() != null) { + fields.put("comments", accountRequest.getComments()); + } + if (accountRequest.getStatus() != null) { + fields.put("status", accountRequest.getStatus().toString()); + } return fields; } diff --git a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java index c5dc5d44428..5325836af32 100644 --- a/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java +++ b/src/main/java/teammates/storage/sqlsearch/AccountRequestSearchManager.java @@ -2,6 +2,7 @@ import java.util.Comparator; import java.util.List; +import java.util.UUID; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.response.QueryResponse; @@ -35,9 +36,8 @@ AccountRequestSearchDocument createDocument(AccountRequest accountRequest) { @Override AccountRequest getEntityFromDocument(SolrDocument document) { - String email = (String) document.getFirstValue("email"); - String institute = (String) document.getFirstValue("institute"); - return accountRequestsDb.getAccountRequest(email, institute); + UUID id = UUID.fromString((String) document.getFieldValue("id")); + return accountRequestsDb.getAccountRequest(id); } @Override diff --git a/src/main/java/teammates/ui/constants/ApiStringConst.java b/src/main/java/teammates/ui/constants/ApiStringConst.java new file mode 100644 index 00000000000..5158f2ac964 --- /dev/null +++ b/src/main/java/teammates/ui/constants/ApiStringConst.java @@ -0,0 +1,40 @@ +package teammates.ui.constants; + +import com.fasterxml.jackson.annotation.JsonValue; + +import teammates.common.util.FieldValidator; + +/** + * Special constants used by the back-end. + */ +public enum ApiStringConst { + // CHECKSTYLE.OFF:JavadocVariable + EMAIL_REGEX(escapeRegex(FieldValidator.REGEX_EMAIL)); + // CHECKSTYLE.ON:JavadocVariable + + private final Object value; + + ApiStringConst(Object value) { + this.value = value; + } + + @JsonValue + public Object getValue() { + return value; + } + + /** + * Escape regex pattern strings to ensure the pattern remains valid when converted to JS. + */ + private static String escapeRegex(String regexStr) { + String escapedRegexStr = regexStr; + // Double escape backslashes + escapedRegexStr = escapedRegexStr.replace("\\", "\\\\"); + // Replace possessive zero or more times quantifier *+ that the email pattern uses + // with greedy zero or more times quantifier * + // as possessive quantifiers are not supported in JavaScript + escapedRegexStr = escapedRegexStr.replace("*+", "*"); + return escapedRegexStr; + } + +} diff --git a/src/main/java/teammates/ui/constants/ResourceEndpoints.java b/src/main/java/teammates/ui/constants/ResourceEndpoints.java index 2401fad8efe..3a7a3abe88c 100644 --- a/src/main/java/teammates/ui/constants/ResourceEndpoints.java +++ b/src/main/java/teammates/ui/constants/ResourceEndpoints.java @@ -15,7 +15,9 @@ public enum ResourceEndpoints { ACCOUNT(ResourceURIs.ACCOUNT), ACCOUNT_RESET(ResourceURIs.ACCOUNT_RESET), ACCOUNT_REQUEST(ResourceURIs.ACCOUNT_REQUEST), + ACCOUNT_REQUESTS(ResourceURIs.ACCOUNT_REQUESTS), ACCOUNT_REQUEST_RESET(ResourceURIs.ACCOUNT_REQUEST_RESET), + ACCOUNT_REQUEST_REJECT(ResourceURIs.ACCOUNT_REQUEST_REJECTION), ACCOUNTS(ResourceURIs.ACCOUNTS), RESPONSE_COMMENT(ResourceURIs.RESPONSE_COMMENT), COURSE(ResourceURIs.COURSE), diff --git a/src/main/java/teammates/ui/output/AccountRequestData.java b/src/main/java/teammates/ui/output/AccountRequestData.java index 2d50bcb1360..92dc77ed50a 100644 --- a/src/main/java/teammates/ui/output/AccountRequestData.java +++ b/src/main/java/teammates/ui/output/AccountRequestData.java @@ -2,6 +2,7 @@ import javax.annotation.Nullable; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.attributes.AccountRequestAttributes; import teammates.storage.sqlentity.AccountRequest; @@ -9,36 +10,44 @@ * Output format of account request data. */ public class AccountRequestData extends ApiOutput { - + private final String id; private final String email; private final String name; private final String institute; private final String registrationKey; + private final AccountRequestStatus status; + @Nullable + private final String comments; @Nullable private final Long registeredAt; private final long createdAt; public AccountRequestData(AccountRequestAttributes accountRequestInfo) { - + this.id = accountRequestInfo.getId(); this.name = accountRequestInfo.getName(); this.email = accountRequestInfo.getEmail(); this.institute = accountRequestInfo.getInstitute(); this.registrationKey = accountRequestInfo.getRegistrationKey(); + this.comments = null; this.createdAt = accountRequestInfo.getCreatedAt().toEpochMilli(); if (accountRequestInfo.getRegisteredAt() == null) { + this.status = AccountRequestStatus.APPROVED; this.registeredAt = null; } else { + this.status = AccountRequestStatus.REGISTERED; this.registeredAt = accountRequestInfo.getRegisteredAt().toEpochMilli(); } } public AccountRequestData(AccountRequest accountRequest) { - + this.id = accountRequest.getId().toString(); this.name = accountRequest.getName(); this.email = accountRequest.getEmail(); this.institute = accountRequest.getInstitute(); this.registrationKey = accountRequest.getRegistrationKey(); + this.status = accountRequest.getStatus(); + this.comments = accountRequest.getComments(); this.createdAt = accountRequest.getCreatedAt().toEpochMilli(); if (accountRequest.getRegisteredAt() == null) { @@ -48,6 +57,10 @@ public AccountRequestData(AccountRequest accountRequest) { } } + public String getId() { + return id; + } + public String getInstitute() { return institute; } @@ -64,6 +77,14 @@ public String getRegistrationKey() { return registrationKey; } + public AccountRequestStatus getStatus() { + return status; + } + + public String getComments() { + return comments; + } + public Long getRegisteredAt() { return registeredAt; } diff --git a/src/main/java/teammates/ui/request/AccountCreateRequest.java b/src/main/java/teammates/ui/request/AccountCreateRequest.java index f3097ce1152..9e39b524549 100644 --- a/src/main/java/teammates/ui/request/AccountCreateRequest.java +++ b/src/main/java/teammates/ui/request/AccountCreateRequest.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; + import teammates.common.util.FieldValidator; import teammates.common.util.StringHelper; @@ -14,6 +16,8 @@ public class AccountCreateRequest extends BasicRequest { private String instructorEmail; private String instructorName; private String instructorInstitution; + @Nullable + private String instructorComments; public String getInstructorEmail() { return instructorEmail; @@ -27,6 +31,10 @@ public String getInstructorInstitution() { return this.instructorInstitution; } + public String getInstructorComments() { + return this.instructorComments; + } + public void setInstructorName(String name) { this.instructorName = name; } @@ -39,6 +47,10 @@ public void setInstructorEmail(String instructorEmail) { this.instructorEmail = instructorEmail; } + public void setInstructorComments(String instructorComments) { + this.instructorComments = instructorComments; + } + @Override public void validate() throws InvalidHttpRequestBodyException { assertTrue(this.instructorEmail != null, "email cannot be null"); diff --git a/src/main/java/teammates/ui/request/AccountRequestRejectionRequest.java b/src/main/java/teammates/ui/request/AccountRequestRejectionRequest.java new file mode 100644 index 00000000000..89884e13b7d --- /dev/null +++ b/src/main/java/teammates/ui/request/AccountRequestRejectionRequest.java @@ -0,0 +1,46 @@ +package teammates.ui.request; + +import java.util.Objects; + +import javax.annotation.Nullable; + +import teammates.common.util.SanitizationHelper; + +/** + * The request reasonBody for rejecting an account request. + */ +public class AccountRequestRejectionRequest extends BasicRequest { + @Nullable + private String reasonTitle; + + @Nullable + private String reasonBody; + + public AccountRequestRejectionRequest(String reasonTitle, String reasonBody) { + this.reasonTitle = SanitizationHelper.sanitizeTitle(reasonTitle); + this.reasonBody = SanitizationHelper.sanitizeForRichText(reasonBody); + } + + @Override + public void validate() throws InvalidHttpRequestBodyException { + if (reasonBody == null || reasonTitle == null) { + assertTrue(Objects.equals(reasonBody, reasonTitle), + "Both reason body and title need to be null to reject silently"); + } + } + + public String getReasonTitle() { + return this.reasonTitle; + } + + public String getReasonBody() { + return this.reasonBody; + } + + /** + * Returns true if both reason body and title are non-null. + */ + public boolean checkHasReason() { + return this.reasonBody != null && this.reasonTitle != null; + } +} diff --git a/src/main/java/teammates/ui/request/AccountRequestUpdateRequest.java b/src/main/java/teammates/ui/request/AccountRequestUpdateRequest.java new file mode 100644 index 00000000000..cc653d79ddb --- /dev/null +++ b/src/main/java/teammates/ui/request/AccountRequestUpdateRequest.java @@ -0,0 +1,63 @@ +package teammates.ui.request; + +import javax.annotation.Nullable; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.SanitizationHelper; + +/** + * The create request for an account request update request. + */ +public class AccountRequestUpdateRequest extends BasicRequest { + private String name; + private String email; + private String institute; + private AccountRequestStatus status; + + @Nullable + private String comments; + + public AccountRequestUpdateRequest(String name, String email, String institute, AccountRequestStatus status, + String comments) { + this.name = SanitizationHelper.sanitizeName(name); + this.email = SanitizationHelper.sanitizeEmail(email); + this.institute = SanitizationHelper.sanitizeName(institute); + this.status = status; + if (comments != null) { + this.comments = SanitizationHelper.sanitizeTextField(comments); + } + } + + @Override + public void validate() throws InvalidHttpRequestBodyException { + assertTrue(name != null, "name cannot be null"); + assertTrue(email != null, "email cannot be null"); + assertTrue(institute != null, "institute cannot be null"); + assertTrue(status != null, "status cannot be null"); + assertTrue(status == AccountRequestStatus.APPROVED + || status == AccountRequestStatus.REJECTED + || status == AccountRequestStatus.PENDING + || status == AccountRequestStatus.REGISTERED, + "status must be one of the following: APPROVED, REJECTED, PENDING, REGISTERED"); + } + + public String getName() { + return this.name; + } + + public String getEmail() { + return this.email; + } + + public String getInstitute() { + return this.institute; + } + + public AccountRequestStatus getStatus() { + return this.status; + } + + public String getComments() { + return this.comments; + } +} diff --git a/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java b/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java index e543b012db4..214ca01f8fd 100644 --- a/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java +++ b/src/main/java/teammates/ui/webapi/AccountRequestSearchIndexingWorkerAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import org.apache.http.HttpStatus; import teammates.common.exception.SearchServiceException; @@ -13,10 +15,16 @@ public class AccountRequestSearchIndexingWorkerAction extends AdminOnlyAction { @Override public ActionResult execute() { - String email = getNonNullRequestParamValue(ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(ParamsNames.INSTRUCTOR_INSTITUTION); + String id = getNonNullRequestParamValue(ParamsNames.ACCOUNT_REQUEST_ID); + UUID accountRequestId; + + try { + accountRequestId = UUID.fromString(id); + } catch (IllegalArgumentException e) { + throw new InvalidHttpParameterException(e.getMessage(), e); + } - AccountRequest accRequest = sqlLogic.getAccountRequest(email, institute); + AccountRequest accRequest = sqlLogic.getAccountRequest(accountRequestId); try { sqlLogic.putAccountRequestDocument(accRequest); diff --git a/src/main/java/teammates/ui/webapi/ActionFactory.java b/src/main/java/teammates/ui/webapi/ActionFactory.java index 38c4b00b753..ae834448b7a 100644 --- a/src/main/java/teammates/ui/webapi/ActionFactory.java +++ b/src/main/java/teammates/ui/webapi/ActionFactory.java @@ -50,7 +50,10 @@ public final class ActionFactory { map(ResourceURIs.ACCOUNT_REQUEST, GET, GetAccountRequestAction.class); map(ResourceURIs.ACCOUNT_REQUEST, POST, CreateAccountRequestAction.class); map(ResourceURIs.ACCOUNT_REQUEST, DELETE, DeleteAccountRequestAction.class); + map(ResourceURIs.ACCOUNT_REQUEST, PUT, UpdateAccountRequestAction.class); + map(ResourceURIs.ACCOUNT_REQUESTS, GET, GetAccountRequestsAction.class); map(ResourceURIs.ACCOUNT_REQUEST_RESET, PUT, ResetAccountRequestAction.class); + map(ResourceURIs.ACCOUNT_REQUEST_REJECTION, POST, RejectAccountRequestAction.class); map(ResourceURIs.ACCOUNTS, GET, GetAccountsAction.class); map(ResourceURIs.COURSE, GET, GetCourseAction.class); map(ResourceURIs.COURSE, DELETE, DeleteCourseAction.class); diff --git a/src/main/java/teammates/ui/webapi/CreateAccountAction.java b/src/main/java/teammates/ui/webapi/CreateAccountAction.java index 8f12a48d88c..3e5ce9d3942 100644 --- a/src/main/java/teammates/ui/webapi/CreateAccountAction.java +++ b/src/main/java/teammates/ui/webapi/CreateAccountAction.java @@ -9,6 +9,7 @@ import org.apache.http.HttpStatus; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.DataBundle; import teammates.common.datatransfer.attributes.InstructorAttributes; import teammates.common.datatransfer.attributes.StudentAttributes; @@ -110,6 +111,7 @@ public JsonResult execute() throws InvalidHttpRequestBodyException, InvalidOpera */ private AccountRequest setAccountRequestAsRegistered(AccountRequest accountRequest) throws InvalidParametersException, EntityDoesNotExistException { + accountRequest.setStatus(AccountRequestStatus.REGISTERED); accountRequest.setRegisteredAt(Instant.now()); sqlLogic.updateAccountRequest(accountRequest); return accountRequest; diff --git a/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java b/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java index f0f214a04f9..f8ba4d571c0 100644 --- a/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/CreateAccountRequestAction.java @@ -1,16 +1,27 @@ package teammates.ui.webapi; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.InvalidParametersException; import teammates.common.util.EmailWrapper; import teammates.storage.sqlentity.AccountRequest; -import teammates.ui.output.JoinLinkData; +import teammates.ui.output.AccountRequestData; import teammates.ui.request.AccountCreateRequest; import teammates.ui.request.InvalidHttpRequestBodyException; /** * Creates a new account request. */ -public class CreateAccountRequestAction extends AdminOnlyAction { +public class CreateAccountRequestAction extends Action { + + @Override + AuthType getMinAuthLevel() { + return AuthType.PUBLIC; + } + + @Override + void checkSpecificAccessControl() throws UnauthorizedAccessException { + // Nothing needs to be done here because anybody should be able to create an account request. + } @Override public boolean isTransactionNeeded() { @@ -25,29 +36,32 @@ public JsonResult execute() String instructorName = createRequest.getInstructorName().trim(); String instructorEmail = createRequest.getInstructorEmail().trim(); String instructorInstitution = createRequest.getInstructorInstitution().trim(); - + String comments = createRequest.getInstructorComments(); + if (comments != null) { + comments = comments.trim(); + } AccountRequest accountRequest; try { - accountRequest = - sqlLogic.createAccountRequestWithTransaction(instructorName, instructorEmail, instructorInstitution); + accountRequest = sqlLogic.createAccountRequestWithTransaction(instructorName, instructorEmail, + instructorInstitution, AccountRequestStatus.PENDING, comments); } catch (InvalidParametersException ipe) { throw new InvalidHttpRequestBodyException(ipe); } - taskQueuer.scheduleAccountRequestForSearchIndexing(instructorEmail, instructorInstitution); + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); - if (accountRequest.getRegisteredAt() != null) { - throw new InvalidOperationException("Cannot create account request as instructor has already registered."); - } + assert accountRequest != null; - String joinLink = accountRequest.getRegistrationUrl(); - - EmailWrapper email = emailGenerator.generateNewInstructorAccountJoinEmail( - instructorEmail, instructorName, joinLink); - emailSender.sendEmail(email); + if (userInfo == null || !userInfo.isAdmin) { + EmailWrapper adminAlertEmail = sqlEmailGenerator.generateNewAccountRequestAdminAlertEmail(accountRequest); + EmailWrapper userAcknowledgementEmail = sqlEmailGenerator + .generateNewAccountRequestAcknowledgementEmail(accountRequest); + emailSender.sendEmail(adminAlertEmail); + emailSender.sendEmail(userAcknowledgementEmail); + } - JoinLinkData output = new JoinLinkData(joinLink); + AccountRequestData output = new AccountRequestData(accountRequest); return new JsonResult(output); } diff --git a/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java b/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java index fa12bc67d81..ad157b1e5a3 100644 --- a/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/DeleteAccountRequestAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import teammates.common.util.Const; import teammates.storage.sqlentity.AccountRequest; @@ -10,17 +12,16 @@ class DeleteAccountRequestAction extends AdminOnlyAction { @Override public JsonResult execute() throws InvalidOperationException { - String email = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); + UUID id = getUuidRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); - AccountRequest toDelete = sqlLogic.getAccountRequest(email, institute); + AccountRequest toDelete = sqlLogic.getAccountRequest(id); if (toDelete != null && toDelete.getRegisteredAt() != null) { // instructor is already registered and cannot be deleted throw new InvalidOperationException("Account request of a registered instructor cannot be deleted."); } - sqlLogic.deleteAccountRequest(email, institute); + sqlLogic.deleteAccountRequest(id); return new JsonResult("Account request successfully deleted."); } diff --git a/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java b/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java index a3f3b5195a4..2894d06b254 100644 --- a/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/GetAccountRequestAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import teammates.common.util.Const; import teammates.storage.sqlentity.AccountRequest; import teammates.ui.output.AccountRequestData; @@ -11,14 +13,12 @@ class GetAccountRequestAction extends AdminOnlyAction { @Override public JsonResult execute() { - String email = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); + UUID id = getUuidRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); - AccountRequest accountRequest = sqlLogic.getAccountRequest(email, institute); + AccountRequest accountRequest = sqlLogic.getAccountRequest(id); if (accountRequest == null) { - throw new EntityNotFoundException("Account request for email: " - + email + " and institute: " + institute + " not found."); + throw new EntityNotFoundException("Account request with id: " + id.toString() + " does not exist."); } AccountRequestData output = new AccountRequestData(accountRequest); diff --git a/src/main/java/teammates/ui/webapi/GetAccountRequestsAction.java b/src/main/java/teammates/ui/webapi/GetAccountRequestsAction.java new file mode 100644 index 00000000000..7cac4331b98 --- /dev/null +++ b/src/main/java/teammates/ui/webapi/GetAccountRequestsAction.java @@ -0,0 +1,34 @@ +package teammates.ui.webapi; + +import java.util.List; +import java.util.stream.Collectors; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.Const; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.output.AccountRequestsData; + +/** + * Action: Gets pending account requests. + */ +public class GetAccountRequestsAction extends AdminOnlyAction { + @Override + public JsonResult execute() { + String accountRequestStatus = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_STATUS); + String pending = AccountRequestStatus.PENDING.name(); // 'PENDING' + if (!pending.equalsIgnoreCase(accountRequestStatus)) { + throw new InvalidHttpParameterException("Only 'pending' is allowed for account request status."); + } + + List accountRequests = sqlLogic.getPendingAccountRequests(); + List accountRequestDatas = accountRequests + .stream() + .map(ar -> new AccountRequestData(ar)) + .collect(Collectors.toList()); + + AccountRequestsData output = new AccountRequestsData(); + output.setAccountRequests(accountRequestDatas); + return new JsonResult(output); + } +} diff --git a/src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java b/src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java new file mode 100644 index 00000000000..f34d6ea3467 --- /dev/null +++ b/src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java @@ -0,0 +1,59 @@ +package teammates.ui.webapi; + +import java.util.UUID; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.EmailWrapper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestRejectionRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; + +/** + * Rejects an account request. + */ +public class RejectAccountRequestAction extends AdminOnlyAction { + + @Override + public boolean isTransactionNeeded() { + return false; + } + + @Override + public JsonResult execute() throws InvalidOperationException, InvalidHttpRequestBodyException { + String id = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); + UUID accountRequestId = getUuidFromString(Const.ParamsNames.ACCOUNT_REQUEST_ID, id); + + AccountRequest accountRequest = sqlLogic.getAccountRequestWithTransaction(accountRequestId); + + if (accountRequest == null) { + String errorMessage = String.format(Const.ACCOUNT_REQUEST_NOT_FOUND, accountRequestId.toString()); + throw new EntityNotFoundException(errorMessage); + } + + AccountRequestRejectionRequest accountRequestRejectionRequest = + getAndValidateRequestBody(AccountRequestRejectionRequest.class); + AccountRequestStatus initialStatus = accountRequest.getStatus(); + + try { + accountRequest.setStatus(AccountRequestStatus.REJECTED); + accountRequest = sqlLogic.updateAccountRequestWithTransaction(accountRequest); + if (accountRequestRejectionRequest.checkHasReason() + && initialStatus != AccountRequestStatus.REJECTED) { + EmailWrapper email = sqlEmailGenerator.generateAccountRequestRejectionEmail(accountRequest, + accountRequestRejectionRequest.getReasonTitle(), accountRequestRejectionRequest.getReasonBody()); + emailSender.sendEmail(email); + } + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); + } catch (InvalidParametersException e) { + throw new InvalidHttpRequestBodyException(e); + } catch (EntityDoesNotExistException e) { + throw new EntityNotFoundException(e); + } + + return new JsonResult(new AccountRequestData(accountRequest)); + } +} diff --git a/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java b/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java index 7fcd3a40c6b..f0ff7ff86b3 100644 --- a/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java +++ b/src/main/java/teammates/ui/webapi/ResetAccountRequestAction.java @@ -1,5 +1,7 @@ package teammates.ui.webapi; +import java.util.UUID; + import org.apache.http.HttpStatus; import teammates.common.exception.EntityDoesNotExistException; @@ -19,21 +21,19 @@ class ResetAccountRequestAction extends AdminOnlyAction { @Override public JsonResult execute() throws InvalidOperationException { - String instructorEmail = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_EMAIL); - String institute = getNonNullRequestParamValue(Const.ParamsNames.INSTRUCTOR_INSTITUTION); + UUID id = getUuidRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); - AccountRequest accountRequest = sqlLogic.getAccountRequest(instructorEmail, institute); + AccountRequest accountRequest = sqlLogic.getAccountRequest(id); if (accountRequest == null) { - throw new EntityNotFoundException("Account request for instructor with email: " + instructorEmail - + " and institute: " + institute + " does not exist."); + throw new EntityNotFoundException("Account request with id: " + id.toString() + " does not exist."); } if (accountRequest.getRegisteredAt() == null) { throw new InvalidOperationException("Unable to reset account request as instructor is still unregistered."); } try { - accountRequest = sqlLogic.resetAccountRequest(instructorEmail, institute); + accountRequest = sqlLogic.resetAccountRequest(id); } catch (InvalidParametersException | EntityDoesNotExistException ue) { // InvalidParametersException and EntityDoesNotExistException should not be thrown as // validity of params has been verified when fetching entity. diff --git a/src/main/java/teammates/ui/webapi/UpdateAccountRequestAction.java b/src/main/java/teammates/ui/webapi/UpdateAccountRequestAction.java new file mode 100644 index 00000000000..416106d32d6 --- /dev/null +++ b/src/main/java/teammates/ui/webapi/UpdateAccountRequestAction.java @@ -0,0 +1,88 @@ +package teammates.ui.webapi; + +import java.util.UUID; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.exception.EntityDoesNotExistException; +import teammates.common.exception.InvalidParametersException; +import teammates.common.util.Const; +import teammates.common.util.EmailWrapper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.ui.output.AccountRequestData; +import teammates.ui.request.AccountRequestUpdateRequest; +import teammates.ui.request.InvalidHttpRequestBodyException; + +/** + * Updates an account request. + */ +public class UpdateAccountRequestAction extends AdminOnlyAction { + + @Override + public boolean isTransactionNeeded() { + return false; + } + + @Override + public JsonResult execute() throws InvalidOperationException, InvalidHttpRequestBodyException { + String id = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID); + UUID accountRequestId = getUuidFromString(Const.ParamsNames.ACCOUNT_REQUEST_ID, id); + + AccountRequest accountRequest = sqlLogic.getAccountRequestWithTransaction(accountRequestId); + + if (accountRequest == null) { + String errorMessage = String.format(Const.ACCOUNT_REQUEST_NOT_FOUND, accountRequestId.toString()); + throw new EntityNotFoundException(errorMessage); + } + + AccountRequestUpdateRequest accountRequestUpdateRequest = + getAndValidateRequestBody(AccountRequestUpdateRequest.class); + + if (accountRequestUpdateRequest.getStatus() == AccountRequestStatus.APPROVED + && (accountRequest.getStatus() == AccountRequestStatus.PENDING + || accountRequest.getStatus() == AccountRequestStatus.REJECTED)) { + + if (sqlLogic.getAccountsForEmailWithTransaction(accountRequest.getEmail()).size() > 0) { + throw new InvalidOperationException(String.format("An account with email %s already exists. " + + "Please reject or delete the account request instead.", + accountRequest.getEmail())); + } + + if (sqlLogic.getApprovedAccountRequestsForEmailWithTransaction(accountRequest.getEmail()).size() > 0) { + throw new InvalidOperationException(String.format( + "An account request with email %s has already been approved. " + + "Please reject or delete the account request instead.", + accountRequest.getEmail())); + } + + try { + // should not need to update other fields for an approval + accountRequest.setStatus(accountRequestUpdateRequest.getStatus()); + accountRequest = sqlLogic.updateAccountRequestWithTransaction(accountRequest); + EmailWrapper email = sqlEmailGenerator.generateNewInstructorAccountJoinEmail( + accountRequest.getEmail(), accountRequest.getName(), accountRequest.getRegistrationUrl()); + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); + emailSender.sendEmail(email); + } catch (InvalidParametersException e) { + throw new InvalidHttpRequestBodyException(e); + } catch (EntityDoesNotExistException e) { + throw new EntityNotFoundException(e); + } + } else { + try { + accountRequest.setName(accountRequestUpdateRequest.getName()); + accountRequest.setEmail(accountRequestUpdateRequest.getEmail()); + accountRequest.setInstitute(accountRequestUpdateRequest.getInstitute()); + accountRequest.setStatus(accountRequest.getStatus()); + accountRequest.setComments(accountRequestUpdateRequest.getComments()); + accountRequest = sqlLogic.updateAccountRequestWithTransaction(accountRequest); + taskQueuer.scheduleAccountRequestForSearchIndexing(accountRequest.getId().toString()); + } catch (InvalidParametersException e) { + throw new InvalidHttpRequestBodyException(e); + } catch (EntityDoesNotExistException e) { + throw new EntityNotFoundException(e); + } + } + + return new JsonResult(new AccountRequestData(accountRequest)); + } +} diff --git a/src/main/resources/adminEmailTemplate-newAccountRequestAlert.html b/src/main/resources/adminEmailTemplate-newAccountRequestAlert.html new file mode 100644 index 00000000000..a91c1250a26 --- /dev/null +++ b/src/main/resources/adminEmailTemplate-newAccountRequestAlert.html @@ -0,0 +1,60 @@ +

    Hello, Admin

    + +

    + A new instructor account request has been submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + ${name} +
    + + Institute + + + ${institute} +
    + + Email Address + + + ${emailAddress} +
    + + Comments + + + ${comments} +
    +
    + +Accept/reject this request on the admin panel: ${adminAccountRequestsPageUrl} + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/main/resources/instructorEmailTemplate-newAccountRequestAcknowledgement.html b/src/main/resources/instructorEmailTemplate-newAccountRequestAcknowledgement.html new file mode 100644 index 00000000000..ad1fe08a704 --- /dev/null +++ b/src/main/resources/instructorEmailTemplate-newAccountRequestAcknowledgement.html @@ -0,0 +1,65 @@ +

    Hello, ${name}

    + +

    + Thank you for submitting an account request. This is what you have submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + ${name} +
    + + Country & Institute + + + ${institute} +
    + + Email Address + + + ${emailAddress} +
    + + Comments + + + ${comments} +
    +
    + +

    + Your request will be reviewed within 24 hours. We will send another email once your request has been accepted. +

    +

    + If you have any additional queries, please feel free to contact us at ${supportEmail}. +

    + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/java/teammates/common/util/FieldValidatorTest.java b/src/test/java/teammates/common/util/FieldValidatorTest.java index e5b3d9748b5..88fc7f33595 100644 --- a/src/test/java/teammates/common/util/FieldValidatorTest.java +++ b/src/test/java/teammates/common/util/FieldValidatorTest.java @@ -210,11 +210,12 @@ public void testGetInvalidityInfoForInstituteName_invalid_returnSpecificErrorStr String invalidInstituteName = StringHelperExtension.generateStringOfLength( FieldValidator.INSTITUTE_NAME_MAX_LENGTH + 1); String actual = FieldValidator.getInvalidityInfoForInstituteName(invalidInstituteName); + String expectedTemplate = "\"%s\" is not " + + "acceptable to TEAMMATES as a/an institute name because it is too long. The value " + + "of a/an institute name should be no longer than 128 characters. It should not be empty."; + String expected = String.format(expectedTemplate, invalidInstituteName); assertEquals("Invalid institute name (too long) should return error message that is specific to institute name", - "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" is not " - + "acceptable to TEAMMATES as a/an institute name because it is too long. The value " - + "of a/an institute name should be no longer than 64 characters. It should not be empty.", - actual); + expected, actual); } @Test diff --git a/src/test/java/teammates/logic/api/MockUserProvision.java b/src/test/java/teammates/logic/api/MockUserProvision.java index 7fa2fdb97f7..f6a88082bcd 100644 --- a/src/test/java/teammates/logic/api/MockUserProvision.java +++ b/src/test/java/teammates/logic/api/MockUserProvision.java @@ -30,6 +30,22 @@ public UserInfo loginUser(String userId) { return loginUser(userId, false); } + private UserInfo loginUserWithTransaction(String userId, boolean isAdmin) { + isLoggedIn = true; + mockUser.id = userId; + mockUser.isAdmin = isAdmin; + return getCurrentUserWithTransaction(null); + } + + /** + * Adds a logged-in user without admin rights. + * + * @return The user info after login process + */ + public UserInfo loginUserWithTransaction(String userId) { + return loginUserWithTransaction(userId, false); + } + /** * Adds a logged-in user as an admin. * @@ -39,6 +55,15 @@ public UserInfo loginAsAdmin(String userId) { return loginUser(userId, true); } + /** + * Adds a logged-in user as an admin. + * + * @return The user info after login process + */ + public UserInfo loginAsAdminWithTransaction(String userId) { + return loginUserWithTransaction(userId, true); + } + /** * Removes the logged-in user information. */ diff --git a/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java b/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java index 88cfd8430b3..24a66afbe6d 100644 --- a/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java +++ b/src/test/java/teammates/logic/core/AccountRequestsLogicTest.java @@ -9,6 +9,7 @@ import teammates.common.exception.InvalidParametersException; import teammates.common.util.Const; import teammates.common.util.FieldValidator; +import teammates.storage.entity.AccountRequest; import teammates.test.AssertHelper; /** @@ -108,7 +109,10 @@ public void testUpdateAccountRequest() throws Exception { @Test public void testDeleteAccountRequest() throws Exception { - AccountRequestAttributes a = dataBundle.accountRequests.get("unregisteredInstructor1"); + // This ensures the AccountRequestAttributes has the correct ID. + AccountRequestAttributes accountRequestAttributes = dataBundle.accountRequests.get("unregisteredInstructor1"); + AccountRequest accountRequest = accountRequestAttributes.toEntity(); + AccountRequestAttributes a = AccountRequestAttributes.valueOf(accountRequest); ______TS("silent deletion of non-existent account request"); diff --git a/src/test/java/teammates/sqllogic/api/SqlEmailGeneratorTest.java b/src/test/java/teammates/sqllogic/api/SqlEmailGeneratorTest.java new file mode 100644 index 00000000000..f984b10f46c --- /dev/null +++ b/src/test/java/teammates/sqllogic/api/SqlEmailGeneratorTest.java @@ -0,0 +1,122 @@ +package teammates.sqllogic.api; + +import java.io.IOException; + +import org.testng.annotations.Test; + +import teammates.common.datatransfer.AccountRequestStatus; +import teammates.common.util.Config; +import teammates.common.util.EmailType; +import teammates.common.util.EmailWrapper; +import teammates.storage.sqlentity.AccountRequest; +import teammates.test.BaseTestCase; +import teammates.test.EmailChecker; + +/** + * SUT: {@link SqlEmailGenerator}. + */ +public class SqlEmailGeneratorTest extends BaseTestCase { + private final SqlEmailGenerator sqlEmailGenerator = SqlEmailGenerator.inst(); + + @Test + void testGenerateNewAccountRequestAdminAlertEmail_withComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("chosen-one@jedi.org", "Anakin Skywalker", "Jedi Order", + AccountRequestStatus.PENDING, + "I don't like sand. It's coarse and rough and irritating... and it gets everywhere."); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAdminAlertEmail(accountRequest); + verifyEmail(email, Config.SUPPORT_EMAIL, EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, + "TEAMMATES (Action Needed): New Account Request Received", + "/adminNewAccountRequestAlertEmailWithComments.html"); + } + + @Test + void testGenerateNewAccountRequestAdminAlertEmail_withNoComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("maul@sith.org", "Maul", "Sith Order", + AccountRequestStatus.PENDING, null); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAdminAlertEmail(accountRequest); + verifyEmail(email, Config.SUPPORT_EMAIL, EmailType.NEW_ACCOUNT_REQUEST_ADMIN_ALERT, + "TEAMMATES (Action Needed): New Account Request Received", + "/adminNewAccountRequestAlertEmailWithNoComments.html"); + } + + @Test + void testGenerateNewAccountRequestAcknowledgementEmail_withComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("darth-vader@sith.org", "Darth Vader", "Sith Order", + AccountRequestStatus.PENDING, + "I Am Your Father"); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAcknowledgementEmail(accountRequest); + verifyEmail(email, "darth-vader@sith.org", EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, + "TEAMMATES: Acknowledgement of Instructor Account Request", + Config.SUPPORT_EMAIL, + "/instructorNewAccountRequestAcknowledgementEmailWithComments.html"); + } + + @Test + void testGenerateNewAccountRequestAcknowledgementEmail_withNoComments_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("maul@sith.org", "Maul", "Sith Order", + AccountRequestStatus.PENDING, null); + EmailWrapper email = sqlEmailGenerator.generateNewAccountRequestAcknowledgementEmail(accountRequest); + verifyEmail(email, "maul@sith.org", EmailType.NEW_ACCOUNT_REQUEST_ACKNOWLEDGEMENT, + "TEAMMATES: Acknowledgement of Instructor Account Request", + Config.SUPPORT_EMAIL, + "/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html"); + } + + @Test + void testGenerateAccountRequestRejectionEmail_withDefaultReason_generatesSuccessfully() throws IOException { + AccountRequest accountRequest = new AccountRequest("maul@sith.org", "Maul", "Sith Order", + AccountRequestStatus.PENDING, null); + String title = "We are Unable to Create an Account for you"; + String content = new StringBuilder() + .append("

    Hi, Maul

    \n") + .append("

    Thanks for your interest in using TEAMMATES. ") + .append("We are unable to create a TEAMMATES instructor account for you.

    \n\n") + .append("

    \n") + .append(" Reason: The email address you provided ") + .append("is not an 'official' email address provided by your institution.
    \n") + .append(" Remedy: ") + .append("Please re-submit an account request with your 'official' institution email address.\n") + .append("

    \n\n") + .append("

    If you need further clarification or would like to appeal this decision, ") + .append("please feel free to contact us at teammates@comp.nus.edu.sg.

    \n") + .append("

    Regards,
    TEAMMATES Team.

    \n") + .toString(); + + EmailWrapper email = sqlEmailGenerator.generateAccountRequestRejectionEmail(accountRequest, title, content); + verifyEmail(email, "maul@sith.org", EmailType.ACCOUNT_REQUEST_REJECTION, + "TEAMMATES: " + title, + Config.SUPPORT_EMAIL, + "/instructorAccountRequestRejectionEmail.html"); + } + + private void verifyEmail(EmailWrapper email, String expectedRecipientEmailAddress, EmailType expectedEmailType, + String expectedSubject, String expectedEmailContentFilePathname) throws IOException { + assertEquals(expectedRecipientEmailAddress, email.getRecipient()); + assertEquals(Config.EMAIL_SENDEREMAIL, email.getSenderEmail()); + assertEquals(Config.EMAIL_SENDERNAME, email.getSenderName()); + assertEquals(Config.EMAIL_REPLYTO, email.getReplyTo()); + assertEquals(expectedEmailType, email.getType()); + assertEquals(expectedSubject, email.getSubject()); + String emailContent = email.getContent(); + EmailChecker.verifyEmailContent(emailContent, expectedEmailContentFilePathname); + verifyEmailContentHasNoPlaceholders(emailContent); + } + + private void verifyEmail(EmailWrapper email, String expectedRecipientEmailAddress, EmailType expectedEmailType, + String expectedSubject, String expectedBcc, String expectedEmailContentFilePathname) throws IOException { + assertEquals(expectedRecipientEmailAddress, email.getRecipient()); + assertEquals(Config.EMAIL_SENDEREMAIL, email.getSenderEmail()); + assertEquals(Config.EMAIL_SENDERNAME, email.getSenderName()); + assertEquals(Config.EMAIL_REPLYTO, email.getReplyTo()); + assertEquals(expectedEmailType, email.getType()); + assertEquals(expectedSubject, email.getSubject()); + assertEquals(expectedBcc, email.getBcc()); + String emailContent = email.getContent(); + EmailChecker.verifyEmailContent(emailContent, expectedEmailContentFilePathname); + verifyEmailContentHasNoPlaceholders(emailContent); + } + + private void verifyEmailContentHasNoPlaceholders(String emailContent) { + assertFalse(emailContent.contains("${")); + } +} diff --git a/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java b/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java index aca37f99963..a0a3064ae32 100644 --- a/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java +++ b/src/test/java/teammates/sqllogic/core/AccountRequestsLogicTest.java @@ -7,10 +7,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.UUID; + import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.util.Const; @@ -23,86 +25,90 @@ */ public class AccountRequestsLogicTest extends BaseTestCase { - private final AccountRequestsLogic arLogic = AccountRequestsLogic.inst(); - private AccountRequestsDb arDb; + private AccountRequestsLogic accountRequestsLogic = AccountRequestsLogic.inst(); + private AccountRequestsDb accountRequestsDb; @BeforeMethod public void setUpMethod() { - arDb = mock(AccountRequestsDb.class); - arLogic.initLogicDependencies(arDb); + accountRequestsDb = mock(AccountRequestsDb.class); + accountRequestsLogic.initLogicDependencies(accountRequestsDb); } @Test public void testCreateAccountRequest_typicalRequest_success() throws Exception { AccountRequest accountRequest = getTypicalAccountRequest(); - when(arDb.createAccountRequest(accountRequest)).thenReturn(accountRequest); - AccountRequest createdAccountRequest = arLogic.createAccountRequest(accountRequest); + when(accountRequestsDb.createAccountRequest(accountRequest)).thenReturn(accountRequest); + AccountRequest createdAccountRequest = accountRequestsLogic.createAccountRequest(accountRequest); assertEquals(accountRequest, createdAccountRequest); - verify(arDb, times(1)).createAccountRequest(accountRequest); + verify(accountRequestsDb, times(1)).createAccountRequest(accountRequest); } @Test - public void testCreateAccountRequest_requestAlreadyExists_failure() throws Exception { - AccountRequest duplicateAccountRequest = getTypicalAccountRequest(); - when(arDb.createAccountRequest(duplicateAccountRequest)) - .thenThrow(new EntityAlreadyExistsException("test exception")); - - assertThrows(EntityAlreadyExistsException.class, () -> { - arLogic.createAccountRequest(duplicateAccountRequest); - }); - verify(arDb, times(1)).createAccountRequest(duplicateAccountRequest); + public void testCreateAccountRequest_requestAlreadyExists_success() throws Exception { + AccountRequest accountRequest1 = getTypicalAccountRequest(); + AccountRequest accountRequest2 = getTypicalAccountRequest(); + when(accountRequestsDb.createAccountRequest(accountRequest1)) + .thenReturn(accountRequest1); + when(accountRequestsDb.createAccountRequest(accountRequest2)) + .thenReturn(accountRequest2); + + accountRequestsLogic.createAccountRequest(accountRequest1); + accountRequestsLogic.createAccountRequest(accountRequest2); + verify(accountRequestsDb, times(1)).createAccountRequest(accountRequest1); + verify(accountRequestsDb, times(1)).createAccountRequest(accountRequest2); } @Test public void testCreateAccountRequest_invalidParams_failure() throws Exception { AccountRequest invalidEmailAccountRequest = getTypicalAccountRequest(); invalidEmailAccountRequest.setEmail("invalid email"); - when(arDb.createAccountRequest(invalidEmailAccountRequest)) + when(accountRequestsDb.createAccountRequest(invalidEmailAccountRequest)) .thenThrow(new InvalidParametersException("test exception")); assertThrows(InvalidParametersException.class, () -> { - arLogic.createAccountRequest(invalidEmailAccountRequest); + accountRequestsLogic.createAccountRequest(invalidEmailAccountRequest); }); - verify(arDb, times(1)).createAccountRequest(invalidEmailAccountRequest); + verify(accountRequestsDb, times(1)).createAccountRequest(invalidEmailAccountRequest); } @Test public void testUpdateAccountRequest_typicalRequest_success() throws InvalidParametersException, EntityDoesNotExistException { AccountRequest ar = getTypicalAccountRequest(); - when(arDb.updateAccountRequest(ar)).thenReturn(ar); - AccountRequest updatedAr = arLogic.updateAccountRequest(ar); + when(accountRequestsDb.updateAccountRequest(ar)).thenReturn(ar); + AccountRequest updatedAr = accountRequestsLogic.updateAccountRequest(ar); assertEquals(ar, updatedAr); - verify(arDb, times(1)).updateAccountRequest(ar); + verify(accountRequestsDb, times(1)).updateAccountRequest(ar); } @Test public void testUpdateAccountRequest_requestNotFound_failure() throws InvalidParametersException, EntityDoesNotExistException { AccountRequest arNotFound = getTypicalAccountRequest(); - when(arDb.updateAccountRequest(arNotFound)).thenThrow(new EntityDoesNotExistException("test message")); + when(accountRequestsDb.updateAccountRequest(arNotFound)).thenThrow(new EntityDoesNotExistException("test message")); assertThrows(EntityDoesNotExistException.class, - () -> arLogic.updateAccountRequest(arNotFound)); - verify(arDb, times(1)).updateAccountRequest(any(AccountRequest.class)); + () -> accountRequestsLogic.updateAccountRequest(arNotFound)); + verify(accountRequestsDb, times(1)).updateAccountRequest(any(AccountRequest.class)); } @Test public void testDeleteAccountRequest_typicalRequest_success() { AccountRequest ar = getTypicalAccountRequest(); - when(arDb.getAccountRequest(ar.getEmail(), ar.getInstitute())).thenReturn(ar); - arLogic.deleteAccountRequest(ar.getEmail(), ar.getInstitute()); + when(accountRequestsDb.getAccountRequest(ar.getId())).thenReturn(ar); + accountRequestsLogic.deleteAccountRequest(ar.getId()); - verify(arDb, times(1)).deleteAccountRequest(any(AccountRequest.class)); + verify(accountRequestsDb, times(1)).deleteAccountRequest(any(AccountRequest.class)); } @Test public void testDeleteAccountRequest_nonexistentRequest_shouldSilentlyDelete() { - arLogic.deleteAccountRequest("not_exist", "not_exist"); + UUID nonexistentUuid = UUID.fromString("00000000-0000-4000-8000-000000000100"); + accountRequestsLogic.deleteAccountRequest(nonexistentUuid); - verify(arDb, times(1)).deleteAccountRequest(nullable(AccountRequest.class)); + verify(accountRequestsDb, times(1)).deleteAccountRequest(nullable(AccountRequest.class)); } @Test @@ -110,42 +116,21 @@ public void testGetAccountRequestByRegistrationKey_typicalRequest_success() { AccountRequest ar = getTypicalAccountRequest(); String regkey = "regkey"; ar.setRegistrationKey(regkey); - when(arDb.getAccountRequestByRegistrationKey(regkey)).thenReturn(ar); + when(accountRequestsDb.getAccountRequestByRegistrationKey(regkey)).thenReturn(ar); AccountRequest actualAr = - arLogic.getAccountRequestByRegistrationKey(ar.getRegistrationKey()); + accountRequestsLogic.getAccountRequestByRegistrationKey(ar.getRegistrationKey()); assertEquals(ar, actualAr); - verify(arDb, times(1)).getAccountRequestByRegistrationKey(regkey); + verify(accountRequestsDb, times(1)).getAccountRequestByRegistrationKey(regkey); } @Test public void testGetAccountRequestByRegistrationKey_nonexistentRequest_shouldReturnNull() throws Exception { String nonexistentRegkey = "not_exist"; - when(arDb.getAccountRequestByRegistrationKey(nonexistentRegkey)).thenReturn(null); - - assertNull(arLogic.getAccountRequestByRegistrationKey(nonexistentRegkey)); - verify(arDb, times(1)).getAccountRequestByRegistrationKey(nonexistentRegkey); - } - - @Test - public void testGetAccountRequest_typicalRequest_success() { - AccountRequest expectedAr = getTypicalAccountRequest(); - when(arDb.getAccountRequest(expectedAr.getEmail(), expectedAr.getInstitute())).thenReturn(expectedAr); - AccountRequest actualAr = - arLogic.getAccountRequest(expectedAr.getEmail(), expectedAr.getInstitute()); - - assertEquals(expectedAr, actualAr); - verify(arDb, times(1)).getAccountRequest(expectedAr.getEmail(), expectedAr.getInstitute()); - } - - @Test - public void testGetAccountRequest_nonexistentRequest_shouldReturnNull() { - String nonexistentEmail = "not-found@test.com"; - String nonexistentInstitute = "not-found"; - when(arDb.getAccountRequest(nonexistentEmail, nonexistentInstitute)).thenReturn(null); + when(accountRequestsDb.getAccountRequestByRegistrationKey(nonexistentRegkey)).thenReturn(null); - assertNull(arLogic.getAccountRequest(nonexistentEmail, nonexistentInstitute)); - verify(arDb, times(1)).getAccountRequest(nonexistentEmail, nonexistentInstitute); + assertNull(accountRequestsLogic.getAccountRequestByRegistrationKey(nonexistentRegkey)); + verify(accountRequestsDb, times(1)).getAccountRequestByRegistrationKey(nonexistentRegkey); } @Test @@ -153,13 +138,13 @@ public void testResetAccountRequest_typicalRequest_success() throws InvalidParametersException, EntityDoesNotExistException { AccountRequest accountRequest = getTypicalAccountRequest(); accountRequest.setRegisteredAt(Const.TIME_REPRESENTS_NOW); - when(arDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute())) + when(accountRequestsDb.getAccountRequest(accountRequest.getId())) .thenReturn(accountRequest); - when(arDb.updateAccountRequest(accountRequest)).thenReturn(accountRequest); - accountRequest = arLogic.resetAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + when(accountRequestsDb.updateAccountRequest(accountRequest)).thenReturn(accountRequest); + accountRequest = accountRequestsLogic.resetAccountRequest(accountRequest.getId()); assertNull(accountRequest.getRegisteredAt()); - verify(arDb, times(1)).getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); + verify(accountRequestsDb, times(1)).getAccountRequest(accountRequest.getId()); } @Test @@ -167,11 +152,31 @@ public void testResetAccountRequest_nonexistentRequest_failure() throws InvalidParametersException, EntityDoesNotExistException { AccountRequest accountRequest = getTypicalAccountRequest(); accountRequest.setRegisteredAt(Const.TIME_REPRESENTS_NOW); - when(arDb.getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute())) + when(accountRequestsDb.getAccountRequest(accountRequest.getId())) .thenReturn(null); assertThrows(EntityDoesNotExistException.class, - () -> arLogic.resetAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute())); - verify(arDb, times(1)).getAccountRequest(accountRequest.getEmail(), accountRequest.getInstitute()); - verify(arDb, times(0)).updateAccountRequest(nullable(AccountRequest.class)); + () -> accountRequestsLogic.resetAccountRequest(accountRequest.getId())); + verify(accountRequestsDb, times(1)).getAccountRequest(accountRequest.getId()); + verify(accountRequestsDb, times(0)).updateAccountRequest(nullable(AccountRequest.class)); + } + + @Test + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + when(accountRequestsDb.getAccountRequest(id)).thenReturn(null); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + verify(accountRequestsDb).getAccountRequest(id); + assertNull(actualAccountRequest); + } + + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + when(accountRequestsDb.getAccountRequest(id)).thenReturn(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestsLogic.getAccountRequest(id); + verify(accountRequestsDb).getAccountRequest(id); + assertEquals(expectedAccountRequest, actualAccountRequest); } } diff --git a/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java b/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java index ef31306aef9..1044cb034c4 100644 --- a/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java +++ b/src/test/java/teammates/storage/sqlapi/AccountRequestsDbTest.java @@ -1,6 +1,5 @@ package teammates.storage.sqlapi; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -10,13 +9,14 @@ import static org.mockito.Mockito.verify; import java.util.List; +import java.util.UUID; import org.mockito.MockedStatic; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import teammates.common.exception.EntityAlreadyExistsException; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.exception.EntityDoesNotExistException; import teammates.common.exception.InvalidParametersException; import teammates.common.exception.SearchServiceException; @@ -49,32 +49,38 @@ public void teardownMethod() { } @Test - public void testCreateAccountRequest_accountRequestDoesNotExist_success() - throws InvalidParametersException, EntityAlreadyExistsException { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(null).when(accountRequestDb).getAccountRequest(anyString(), anyString()); - + public void testCreateAccountRequest_typicalCase_success() throws InvalidParametersException { + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.createAccountRequest(accountRequest); mockHibernateUtil.verify(() -> HibernateUtil.persist(accountRequest)); } @Test - public void testCreateAccountRequest_accountRequestAlreadyExists_throwsEntityAlreadyExistsException() { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(new AccountRequest("test@gmail.com", "name", "institute")) - .when(accountRequestDb).getAccountRequest(anyString(), anyString()); - - EntityAlreadyExistsException ex = assertThrows(EntityAlreadyExistsException.class, - () -> accountRequestDb.createAccountRequest(accountRequest)); + public void testGetAccountRequest_nonExistentAccountRequest_returnsNull() { + UUID id = UUID.randomUUID(); + mockHibernateUtil.when(() -> HibernateUtil.get(AccountRequest.class, id)).thenReturn(null); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + mockHibernateUtil.verify(() -> HibernateUtil.get(AccountRequest.class, id)); + assertNull(actualAccountRequest); + } - assertEquals(ex.getMessage(), "Trying to create an entity that exists: " + accountRequest.toString()); - mockHibernateUtil.verify(() -> HibernateUtil.persist(accountRequest), never()); + @Test + public void testGetAccountRequest_existingAccountRequest_getsSuccessfully() { + AccountRequest expectedAccountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + UUID id = expectedAccountRequest.getId(); + mockHibernateUtil.when(() -> HibernateUtil.get(AccountRequest.class, id)).thenReturn(expectedAccountRequest); + AccountRequest actualAccountRequest = accountRequestDb.getAccountRequest(id); + mockHibernateUtil.verify(() -> HibernateUtil.get(AccountRequest.class, id)); + assertEquals(expectedAccountRequest, actualAccountRequest); } @Test public void testUpdateAccountRequest_invalidEmail_throwsInvalidParametersException() { - AccountRequest accountRequestWithInvalidEmail = new AccountRequest("testgmail.com", "name", "institute"); + AccountRequest accountRequestWithInvalidEmail = + new AccountRequest("testgmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); assertThrows(InvalidParametersException.class, () -> accountRequestDb.updateAccountRequest(accountRequestWithInvalidEmail)); @@ -84,8 +90,9 @@ public void testUpdateAccountRequest_invalidEmail_throwsInvalidParametersExcepti @Test public void testUpdateAccountRequest_accountRequestDoesNotExist_throwsEntityDoesNotExistException() { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(null).when(accountRequestDb).getAccountRequest(anyString(), anyString()); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + doReturn(null).when(accountRequestDb).getAccountRequest(accountRequest.getId()); assertThrows(EntityDoesNotExistException.class, () -> accountRequestDb.updateAccountRequest(accountRequest)); @@ -95,8 +102,9 @@ public void testUpdateAccountRequest_accountRequestDoesNotExist_throwsEntityDoes @Test public void testUpdateAccountRequest_success() throws InvalidParametersException, EntityDoesNotExistException { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); - doReturn(accountRequest).when(accountRequestDb).getAccountRequest(anyString(), anyString()); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); + doReturn(accountRequest).when(accountRequestDb).getAccountRequest(accountRequest.getId()); accountRequestDb.updateAccountRequest(accountRequest); @@ -105,7 +113,8 @@ public void testUpdateAccountRequest_success() throws InvalidParametersException @Test public void testDeleteAccountRequest_success() { - AccountRequest accountRequest = new AccountRequest("test@gmail.com", "name", "institute"); + AccountRequest accountRequest = + new AccountRequest("test@gmail.com", "name", "institute", AccountRequestStatus.PENDING, "comments"); accountRequestDb.deleteAccountRequest(accountRequest); diff --git a/src/test/java/teammates/test/AbstractBackDoor.java b/src/test/java/teammates/test/AbstractBackDoor.java index 16c62c5e3ed..48fb1eec380 100644 --- a/src/test/java/teammates/test/AbstractBackDoor.java +++ b/src/test/java/teammates/test/AbstractBackDoor.java @@ -832,10 +832,9 @@ public void deleteCourse(String courseId) { /** * Gets an account request from the database. */ - public AccountRequestAttributes getAccountRequest(String email, String institute) { + public AccountRequestAttributes getAccountRequest(UUID id) { Map params = new HashMap<>(); - params.put(Const.ParamsNames.INSTRUCTOR_EMAIL, email); - params.put(Const.ParamsNames.INSTRUCTOR_INSTITUTION, institute); + params.put(Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()); ResponseBodyAndCode response = executeGetRequest(Const.ResourceURIs.ACCOUNT_REQUEST, params); if (response.responseCode == HttpStatus.SC_NOT_FOUND) { @@ -852,10 +851,9 @@ public AccountRequestAttributes getAccountRequest(String email, String institute /** * Gets registration key of an account request from the database. */ - public String getRegKeyForAccountRequest(String email, String institute) { + public String getRegKeyForAccountRequest(UUID id) { Map params = new HashMap<>(); - params.put(Const.ParamsNames.INSTRUCTOR_EMAIL, email); - params.put(Const.ParamsNames.INSTRUCTOR_INSTITUTION, institute); + params.put(Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()); ResponseBodyAndCode response = executeGetRequest(Const.ResourceURIs.ACCOUNT_REQUEST, params); if (response.responseCode == HttpStatus.SC_NOT_FOUND) { @@ -868,10 +866,9 @@ public String getRegKeyForAccountRequest(String email, String institute) { /** * Deletes an account request from the database. */ - public void deleteAccountRequest(String email, String institute) { + public void deleteAccountRequest(UUID id) { Map params = new HashMap<>(); - params.put(Const.ParamsNames.INSTRUCTOR_EMAIL, email); - params.put(Const.ParamsNames.INSTRUCTOR_INSTITUTION, institute); + params.put(Const.ParamsNames.ACCOUNT_REQUEST_ID, id.toString()); executeDeleteRequest(Const.ResourceURIs.ACCOUNT_REQUEST, params); } diff --git a/src/test/java/teammates/test/BaseTestCase.java b/src/test/java/teammates/test/BaseTestCase.java index ffe08b21813..c7df328d887 100644 --- a/src/test/java/teammates/test/BaseTestCase.java +++ b/src/test/java/teammates/test/BaseTestCase.java @@ -13,6 +13,7 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import teammates.common.datatransfer.AccountRequestStatus; import teammates.common.datatransfer.DataBundle; import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.datatransfer.InstructorPermissionRole; @@ -208,7 +209,8 @@ protected FeedbackResponseComment getTypicalResponseComment(Long id) { } protected AccountRequest getTypicalAccountRequest() { - return new AccountRequest("valid@test.com", "Test account Name", "TEAMMATES Test Institute 1"); + return new AccountRequest("valid@test.com", "Test Name", "TEAMMATES Test Institute 1, Test Country", + AccountRequestStatus.PENDING, ""); } /** diff --git a/src/test/java/teammates/ui/request/AccountRequestRejectionRequestTest.java b/src/test/java/teammates/ui/request/AccountRequestRejectionRequestTest.java new file mode 100644 index 00000000000..412bfcf1d87 --- /dev/null +++ b/src/test/java/teammates/ui/request/AccountRequestRejectionRequestTest.java @@ -0,0 +1,51 @@ +package teammates.ui.request; + +import org.testng.annotations.Test; + +import teammates.test.BaseTestCase; + +/** + * SUT: {@link AccountRequestRejectionRequest}. + */ +public class AccountRequestRejectionRequestTest extends BaseTestCase { + + private static final String TYPICAL_TITLE = "We are Unable to Create an Account for you"; + private static final String TYPICAL_BODY = new StringBuilder() + .append("

    Hi, Example

    \n") + .append("

    Thanks for your interest in using TEAMMATES. ") + .append("We are unable to create a TEAMMATES instructor account for you.

    \n\n") + .append("

    \n") + .append(" Reason: The email address you provided ") + .append("is not an 'official' email address provided by your institution.
    \n") + .append(" Remedy: ") + .append("Please re-submit an account request with your 'official' institution email address.\n") + .append("

    \n\n") + .append("

    If you need further clarification or would like to appeal this decision, ") + .append("please feel free to contact us at teammates@comp.nus.edu.sg.

    \n") + .append("

    Regards,
    TEAMMATES Team.

    \n") + .toString(); + + @Test + public void testValidate_withNonNullBodyAndNonNullTitle_shouldPass() throws Exception { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(TYPICAL_TITLE, TYPICAL_BODY); + request.validate(); + } + + @Test + public void testValidate_withNullBodyAndNullTitle_shouldPass() throws Exception { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(null, null); + request.validate(); + } + + @Test + public void testValidate_withNonNullBodyAndNullTitle_shouldFail() { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(null, TYPICAL_BODY); + assertThrows(InvalidHttpRequestBodyException.class, request::validate); + } + + @Test + public void testValidate_withNullBodyAndNonNullTitle_shouldFail() { + AccountRequestRejectionRequest request = new AccountRequestRejectionRequest(TYPICAL_TITLE, null); + assertThrows(InvalidHttpRequestBodyException.class, request::validate); + } +} diff --git a/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java b/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java index 1fb58f95c6b..2e1cfa96b05 100644 --- a/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java +++ b/src/test/java/teammates/ui/webapi/CreateAccountRequestActionTest.java @@ -4,9 +4,7 @@ import teammates.common.datatransfer.attributes.AccountRequestAttributes; import teammates.common.util.Const; -import teammates.common.util.EmailType; -import teammates.common.util.EmailWrapper; -import teammates.ui.output.JoinLinkData; +import teammates.ui.output.AccountRequestData; import teammates.ui.request.AccountCreateRequest; import teammates.ui.request.InvalidHttpRequestBodyException; @@ -64,45 +62,23 @@ protected void testExecute() { assertEquals(institute, accountRequestAttributes.getInstitute()); assertNotNull(accountRequestAttributes.getRegistrationKey()); - String joinLink = accountRequestAttributes.getRegistrationUrl(); - JoinLinkData output = (JoinLinkData) r.getOutput(); - assertEquals(joinLink, output.getJoinLink()); + String registrationKey = accountRequestAttributes.getRegistrationKey(); + AccountRequestData output = (AccountRequestData) r.getOutput(); + assertEquals(registrationKey, output.getRegistrationKey()); - verifyNumberOfEmailsSent(1); + verifyNoEmailsSent(); verifySpecifiedTasksAdded(Const.TaskQueue.SEARCH_INDEXING_QUEUE_NAME, 1); - EmailWrapper emailSent = mockEmailSender.getEmailsSent().get(0); - assertEquals(String.format(EmailType.NEW_INSTRUCTOR_ACCOUNT.getSubject(), name), - emailSent.getSubject()); - assertEquals(email, emailSent.getRecipient()); - assertTrue(emailSent.getContent().contains(joinLink)); - - ______TS("Account request already exists: instructor unregistered, email sent again"); + ______TS("Account request already exists: instructor unregistered"); a = getAction(req); r = getJsonResult(a); - output = (JoinLinkData) r.getOutput(); - assertEquals(joinLink, output.getJoinLink()); + output = (AccountRequestData) r.getOutput(); + assertEquals(registrationKey, output.getRegistrationKey()); - verifyNumberOfEmailsSent(1); + verifyNoEmailsSent(); verifyNoTasksAdded(); // Account request not added to search indexing queue - emailSent = mockEmailSender.getEmailsSent().get(0); - assertEquals(String.format(EmailType.NEW_INSTRUCTOR_ACCOUNT.getSubject(), name), - emailSent.getSubject()); - assertEquals(email, emailSent.getRecipient()); - assertTrue(emailSent.getContent().contains(joinLink)); - - ______TS("Account request already exists: instructor registered, InvalidOperationException thrown"); - - accountRequestAttributes = typicalBundle.accountRequests.get("instructor1OfCourse1"); - - req = buildCreateRequest(accountRequestAttributes.getName(), - accountRequestAttributes.getInstitute(), accountRequestAttributes.getEmail()); - - InvalidOperationException ioe = verifyInvalidOperation(req); - assertEquals("Cannot create account request as instructor has already registered.", ioe.getMessage()); - ______TS("Error: invalid parameter"); String invalidName = "James%20Bond99"; diff --git a/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java b/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java index 2c989868edc..fbcd4a0765c 100644 --- a/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java +++ b/src/test/java/teammates/ui/webapi/GetActionClassesActionTest.java @@ -86,6 +86,9 @@ protected void testExecute() { CreateAccountRequestAction.class, GetAccountRequestAction.class, DeleteAccountRequestAction.class, + GetAccountRequestsAction.class, + UpdateAccountRequestAction.class, + RejectAccountRequestAction.class, GetAccountAction.class, GetAccountsAction.class, FeedbackSessionPublishedRemindersAction.class, diff --git a/src/test/resources/emails/adminNewAccountRequestAlertEmailWithComments.html b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithComments.html new file mode 100644 index 00000000000..301fd024a3e --- /dev/null +++ b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithComments.html @@ -0,0 +1,60 @@ +

    Hello, Admin

    + +

    + A new instructor account request has been submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Anakin Skywalker +
    + + Institute + + + Jedi Order +
    + + Email Address + + + chosen-one@jedi.org +
    + + Comments + + + I don't like sand. It's coarse and rough and irritating... and it gets everywhere. +
    +
    + +Accept/reject this request on the admin panel: ${app.url}/web/admin/home + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/resources/emails/adminNewAccountRequestAlertEmailWithNoComments.html b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithNoComments.html new file mode 100644 index 00000000000..a2f62ae17b1 --- /dev/null +++ b/src/test/resources/emails/adminNewAccountRequestAlertEmailWithNoComments.html @@ -0,0 +1,60 @@ +

    Hello, Admin

    + +

    + A new instructor account request has been submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Maul +
    + + Institute + + + Sith Order +
    + + Email Address + + + maul@sith.org +
    + + Comments + + + +
    +
    + +Accept/reject this request on the admin panel: ${app.url}/web/admin/home + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/resources/emails/instructorAccountRequestRejectionEmail.html b/src/test/resources/emails/instructorAccountRequestRejectionEmail.html new file mode 100644 index 00000000000..57ae404c7e4 --- /dev/null +++ b/src/test/resources/emails/instructorAccountRequestRejectionEmail.html @@ -0,0 +1,10 @@ +

    Hi, Maul

    +

    Thanks for your interest in using TEAMMATES. We are unable to create a TEAMMATES instructor account for you.

    + +

    + Reason: The email address you provided is not an 'official' email address provided by your institution.
    + Remedy: Please re-submit an account request with your 'official' institution email address. +

    + +

    If you need further clarification or would like to appeal this decision, please feel free to contact us at teammates@comp.nus.edu.sg.

    +

    Regards,
    TEAMMATES Team.

    diff --git a/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithComments.html b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithComments.html new file mode 100644 index 00000000000..3d634a78864 --- /dev/null +++ b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithComments.html @@ -0,0 +1,65 @@ +

    Hello, Darth Vader

    + +

    + Thank you for submitting an account request. This is what you have submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Darth Vader +
    + + Country & Institute + + + Sith Order +
    + + Email Address + + + darth-vader@sith.org +
    + + Comments + + + I Am Your Father +
    +
    + +

    + Your request will be reviewed within 24 hours. We will send another email once your request has been accepted. +

    +

    + If you have any additional queries, please feel free to contact us at ${support.email}. +

    + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html new file mode 100644 index 00000000000..9460b051df5 --- /dev/null +++ b/src/test/resources/emails/instructorNewAccountRequestAcknowledgementEmailWithNoComments.html @@ -0,0 +1,65 @@ +

    Hello, Maul

    + +

    + Thank you for submitting an account request. This is what you have submitted: +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + Full Name + + + Maul +
    + + Country & Institute + + + Sith Order +
    + + Email Address + + + maul@sith.org +
    + + Comments + + + +
    +
    + +

    + Your request will be reviewed within 24 hours. We will send another email once your request has been accepted. +

    +

    + If you have any additional queries, please feel free to contact us at ${support.email}. +

    + +

    + Regards,
    + TEAMMATES Team. +

    diff --git a/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap b/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap new file mode 100644 index 00000000000..f5364641514 --- /dev/null +++ b/src/web/app/components/account-requests-table/__snapshots__/account-request-table.component.spec.ts.snap @@ -0,0 +1,765 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountRequestTableComponent should display account requests with no reset or expand links button 1`] = ` + +
    +
    + + Pending Account Requests + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Email + + Status + + Institute, Country + + Created At + + Comments + + Options +
    + name + + email + + PENDING + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    +
    + name + + email + + PENDING + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    +
    +
    +
    +
    +`; + +exports[`AccountRequestTableComponent should display account requests with reset button and expandable links buttons 1`] = ` + +
    +
    +
    + + Account Requests Found + +
    +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Email + + Status + + Institute, Country + + Created At + + Registered At + + Comments + + Options +
    + name + + email + + APPROVED + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + + Not Registered Yet + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    + +
    +
    +
    + name + + email + + REGISTERED + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + + Not Registered Yet + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    + +
    +
    +
    +
    +
    +
    +`; + +exports[`AccountRequestTableComponent should snap with an expanded account requests table 1`] = ` + +
    +
    + + Pending Account Requests + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Email + + Status + + Institute, Country + + Created At + + Comments + + Options +
    + name + + email + + PENDING + + institute + + Tue, 08 Feb 2022, 08:23 AM +00:00 + +
    + comment +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + + + +
    +
    +
    +
    +
    +`; diff --git a/src/web/app/components/account-requests-table/account-request-table-model.ts b/src/web/app/components/account-requests-table/account-request-table-model.ts new file mode 100644 index 00000000000..1dc11a3b43c --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table-model.ts @@ -0,0 +1,17 @@ +import { AccountRequestStatus } from 'src/web/types/api-output'; + +/** + * Model for the row entries in the account requests table. + */ +export interface AccountRequestTableRowModel { + id: string; + name: string; + email: string; + status: AccountRequestStatus; + instituteAndCountry: string; + createdAtText: string; + registeredAtText: string; + comments: string; + registrationLink: string; + showLinks: boolean; +} diff --git a/src/web/app/components/account-requests-table/account-request-table.component.html b/src/web/app/components/account-requests-table/account-request-table.component.html new file mode 100644 index 00000000000..9b23db6a3d8 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.html @@ -0,0 +1,96 @@ +
    +
    +
    + Account Requests Found +
    + + Pending Account Requests + +
    + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameEmailStatusInstitute, CountryCreated AtRegistered AtCommentsOptions
    +
    +
    + + +
    +
    {{ accountRequest.email }}{{ accountRequest.status }}{{ accountRequest.instituteAndCountry }}{{ accountRequest.createdAtText }}{{ accountRequest.registeredAtText || 'Not Registered Yet' }} +
    + {{ accountRequest.comments }} +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    + + + +
    + + +
    +
    +
    + +
    +
    +
    +
      +
    • + Account Registration Link + +
    • +
    +
    +
    diff --git a/src/web/app/components/account-requests-table/account-request-table.component.scss b/src/web/app/components/account-requests-table/account-request-table.component.scss new file mode 100644 index 00000000000..6af57f07309 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.scss @@ -0,0 +1,65 @@ +::ng-deep .highlighted-text { + background-color: yellow; +} + +/* stylelint-disable declaration-block-no-redundant-longhand-properties */ +.table-responsive { + overflow-y: visible; + overflow-x: -moz-scrollbars-horizontal; +} + +.table-responsive > table > thead > tr > th { + white-space: nowrap; +} + +/* stylelint-disable property-no-vendor-prefix */ +::-webkit-scrollbar { + -webkit-appearance: none; + width: 1px; +} + +::-webkit-scrollbar-thumb { + border-radius: 0; + background-color: rgb(0 0 0 / 50%); + box-shadow: 0 0 1px rgb(255 255 255 / 50%); +} + + +#search-table-account-request { + border-collapse: collapse; +} + + +#search-table-account-request th:last-child, +#search-table-account-request td:last-child { + min-width: 10vw; + position: sticky; + right: 0; + z-index: 1; + background-color: #F8F9FA; +} + +#search-table-account-request th:last-child::after, +#search-table-account-request td:last-child::after { + content: ""; + position: absolute; + left: -1px; + top: 0; + bottom: 0; + width: 1px; + background: #c8c7c7; + z-index: 1; +} + +#comment-box { + min-height: 5vh; + width: max(800px, 35vw); + max-width: max-content; + word-break: break-word; + word-wrap: break-all; + +} + +.dropdown-item { + border: none; +} diff --git a/src/web/app/components/account-requests-table/account-request-table.component.spec.ts b/src/web/app/components/account-requests-table/account-request-table.component.spec.ts new file mode 100644 index 00000000000..6de7c277b22 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.spec.ts @@ -0,0 +1,497 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { of, throwError } from 'rxjs'; +import { AccountRequestTableRowModel } from './account-request-table-model'; +import { AccountRequestTableComponent } from './account-request-table.component'; +import { AccountRequestTableModule } from './account-request-table.module'; +import { EditRequestModalComponent } from './admin-edit-request-modal/admin-edit-request-modal.component'; +import { + RejectWithReasonModalComponent, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component'; +import { AccountService } from '../../../services/account.service'; +import { SimpleModalService } from '../../../services/simple-modal.service'; +import { StatusMessageService } from '../../../services/status-message.service'; +import { createBuilder } from '../../../test-helpers/generic-builder'; +import { createMockNgbModalRef } from '../../../test-helpers/mock-ngb-modal-ref'; +import { AccountRequest, AccountRequestStatus } from '../../../types/api-output'; +import { SimpleModalType } from '../simple-modal/simple-modal-type'; + +describe('AccountRequestTableComponent', () => { + let component: AccountRequestTableComponent; + let fixture: ComponentFixture; + let accountService: AccountService; + let statusMessageService: StatusMessageService; + let simpleModalService: SimpleModalService; + let ngbModal: NgbModal; + + const accountRequestDetailsBuilder = createBuilder({ + id: '', + email: '', + name: '', + instituteAndCountry: '', + registrationLink: '', + status: AccountRequestStatus.PENDING, + comments: '', + registeredAtText: '', + createdAtText: '', + showLinks: false, + }); + + const DEFAULT_ACCOUNT_REQUEST = accountRequestDetailsBuilder + .email('email') + .name('name') + .status(AccountRequestStatus.PENDING) + .instituteAndCountry('institute') + .createdAtText('Tue, 08 Feb 2022, 08:23 AM +00:00') + .comments('comment'); + + const resetModalContent = `Are you sure you want to reset the account request for + name with email email from + institute? + An email with the account registration link will also be sent to the instructor.`; + const resetModalTitle = 'Reset account request for name?'; + const deleteModalContent = `Are you sure you want to delete the account request for + name with email email from + institute?`; + const deleteModalTitle = 'Delete account request for name?'; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [AccountRequestTableComponent], + imports: [ + AccountRequestTableModule, + BrowserAnimationsModule, + HttpClientTestingModule, + ], + providers: [ + AccountService, SimpleModalService, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AccountRequestTableComponent); + component = fixture.componentInstance; + accountService = TestBed.inject(AccountService); + statusMessageService = TestBed.inject(StatusMessageService); + simpleModalService = TestBed.inject(SimpleModalService); + ngbModal = TestBed.inject(NgbModal); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should snap with an expanded account requests table', () => { + const accountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + component.accountRequests = [ + accountRequestResult, + ]; + + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should show account request links when expand all button clicked', () => { + const accountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + accountRequestResult.status = AccountRequestStatus.APPROVED; + accountRequestResult.registrationLink = 'registrationLink'; + component.accountRequests = [ + accountRequestResult, + ]; + component.searchString = 'test'; + fixture.detectChanges(); + + const button: any = fixture.debugElement.nativeElement.querySelector('#show-account-request-links'); + button.click(); + expect(component.accountRequests[0].showLinks).toEqual(true); + }); + + it('should display account requests with no reset or expand links button', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should display account requests with reset button and expandable links buttons', + () => { + const approvedAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + approvedAccountRequestResult.status = AccountRequestStatus.APPROVED; + approvedAccountRequestResult.registrationLink = 'registrationLink'; + + const registeredAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + registeredAccountRequestResult.status = AccountRequestStatus.REGISTERED; + registeredAccountRequestResult.registrationLink = 'registrationLink'; + + const accountRequestResults: AccountRequestTableRowModel[] = [ + approvedAccountRequestResult, + registeredAccountRequestResult, + ]; + + component.accountRequests = accountRequestResults; + component.searchString = 'test'; + fixture.detectChanges(); + expect(fixture).toMatchSnapshot(); + }); + + it('should show success message when deleting account request is successful', () => { + component.accountRequests = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'deleteAccountRequest').mockReturnValue(of({ + message: 'Account request successfully deleted.', + })); + + const spyStatusMessageService: any = jest.spyOn(statusMessageService, 'showSuccessToast') + .mockImplementation((args: string) => { + expect(args).toEqual('Account request successfully deleted.'); + }); + + const deleteButton: any = fixture.debugElement.nativeElement.querySelector('#delete-account-request-0'); + deleteButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(deleteModalTitle, SimpleModalType.DANGER, deleteModalContent); + }); + + it('should show error message when deleting account request is unsuccessful', () => { + component.accountRequests = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'deleteAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService: any = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const deleteButton: any = fixture.debugElement.nativeElement.querySelector('#delete-account-request-0'); + deleteButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(deleteModalTitle, SimpleModalType.DANGER, deleteModalContent); + }); + + it('should show success message when resetting account request is successful', () => { + const registeredAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + registeredAccountRequestResult.status = AccountRequestStatus.REGISTERED; + registeredAccountRequestResult.registrationLink = 'registrationLink'; + registeredAccountRequestResult.registeredAtText = 'registeredTime'; + component.accountRequests = [ + registeredAccountRequestResult, + ]; + + component.searchString = 'test'; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'resetAccountRequest').mockReturnValue(of({ + joinLink: 'joinlink', + })); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showSuccessToast') + .mockImplementation((args: string) => { + expect(args) + .toEqual('Reset successful. An email has been sent to email.'); + }); + + const resetButton = fixture.debugElement.nativeElement.querySelector('#reset-account-request-0'); + resetButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(resetModalTitle, SimpleModalType.WARNING, resetModalContent); + }); + + it('should show error message when resetting account request is unsuccessful', () => { + const registeredAccountRequestResult: AccountRequestTableRowModel = DEFAULT_ACCOUNT_REQUEST.build(); + registeredAccountRequestResult.status = AccountRequestStatus.REGISTERED; + registeredAccountRequestResult.registrationLink = 'registrationLink'; + registeredAccountRequestResult.registeredAtText = 'registeredTime'; + component.accountRequests = [ + registeredAccountRequestResult, + ]; + + component.searchString = 'test'; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openConfirmationModal').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'resetAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const resetButton = fixture.debugElement.nativeElement.querySelector('#reset-account-request-0'); + resetButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(resetModalTitle, SimpleModalType.WARNING, resetModalContent); + }); + + it('should display comment modal', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(simpleModalService, 'openInformationModal') + .mockReturnValue(createMockNgbModalRef()); + + const viewCommentButton: any = fixture.debugElement.nativeElement.querySelector('#view-account-request-0'); + viewCommentButton.click(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith('Comments for name Request', + SimpleModalType.INFO, 'Comment: comment'); + }); + + it('should display edit modal when edit button is clicked', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(ngbModal, 'open').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + const editButton: any = fixture.debugElement.nativeElement.querySelector('#edit-account-request-0'); + editButton.click(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(EditRequestModalComponent); + }); + + it('should display reject modal when reject button is clicked', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(ngbModal, 'open').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + const rejectButton: any = fixture.debugElement.nativeElement.querySelector('#reject-request-with-reason-0'); + rejectButton.click(); + fixture.detectChanges(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(RejectWithReasonModalComponent); + }); + + it('should display error message when rejection was unsuccessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + jest.spyOn(accountService, 'rejectAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const rejectButton = fixture.debugElement.nativeElement.querySelector('#reject-request-0'); + rejectButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + }); + + it('should display error message when approval was unsuccessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + jest.spyOn(accountService, 'approveAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService: any = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const approveButton: any = fixture.debugElement.nativeElement.querySelector('#approve-account-request-0'); + approveButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + }); + + it('should display error message when edit was unsuccessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + jest.spyOn(ngbModal, 'open').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + jest.spyOn(accountService, 'editAccountRequest').mockReturnValue(throwError(() => ({ + error: { + message: 'This is the error message.', + }, + }))); + + const spyStatusMessageService = jest.spyOn(statusMessageService, 'showErrorToast') + .mockImplementation((args: string) => { + expect(args).toEqual('This is the error message.'); + }); + + const editButton = fixture.debugElement.nativeElement.querySelector('#edit-account-request-0'); + editButton.click(); + + expect(spyStatusMessageService).toHaveBeenCalled(); + }); + + it('should update request when edit is succcessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const modalSpy = jest.spyOn(ngbModal, 'open').mockImplementation(() => { + return createMockNgbModalRef({}); + }); + + const editedAccountRequest : AccountRequest = { + id: 'id', + comments: 'new comment', + email: 'new email', + institute: 'new institute', + registrationKey: 'registration key', + name: 'new name', + createdAt: 1, + status: AccountRequestStatus.PENDING, + }; + + jest.spyOn(accountService, 'editAccountRequest').mockReturnValue(of(editedAccountRequest)); + + const editButton: any = fixture.debugElement.nativeElement.querySelector('#edit-account-request-0'); + editButton.click(); + expect(modalSpy).toHaveBeenCalledTimes(1); + expect(modalSpy).toHaveBeenCalledWith(EditRequestModalComponent); + + fixture.detectChanges(); + expect(component.accountRequests[0].comments).toEqual('new comment'); + expect(component.accountRequests[0].email).toEqual('new email'); + expect(component.accountRequests[0].instituteAndCountry).toEqual('new institute'); + expect(component.accountRequests[0].name).toEqual('new name'); + }); + + it('should update status when approval is succcessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const approvedRequest : AccountRequest = { + id: component.accountRequests[0].id, + comments: component.accountRequests[0].comments, + email: component.accountRequests[0].email, + institute: component.accountRequests[0].instituteAndCountry, + registrationKey: 'registration key', + name: component.accountRequests[0].name, + createdAt: 1, + status: AccountRequestStatus.APPROVED, + }; + + jest.spyOn(accountService, 'approveAccountRequest').mockReturnValue(of(approvedRequest)); + + const approveButton: any = fixture.debugElement.nativeElement.querySelector('#approve-account-request-0'); + approveButton.click(); + + fixture.detectChanges(); + expect(component.accountRequests[0].status).toEqual(AccountRequestStatus.APPROVED); + }); + + it('should update status when rejection is succcessful', () => { + const accountRequestResults: AccountRequestTableRowModel[] = [ + DEFAULT_ACCOUNT_REQUEST.build(), + ]; + + component.accountRequests = accountRequestResults; + fixture.detectChanges(); + + const rejectedRequest : AccountRequest = { + id: component.accountRequests[0].id, + comments: component.accountRequests[0].comments, + email: component.accountRequests[0].email, + institute: component.accountRequests[0].instituteAndCountry, + registrationKey: 'registration key', + name: component.accountRequests[0].name, + createdAt: 1, + status: AccountRequestStatus.REJECTED, + }; + + jest.spyOn(accountService, 'rejectAccountRequest').mockReturnValue(of(rejectedRequest)); + + const rejectButton: any = fixture.debugElement.nativeElement.querySelector('#reject-request-0'); + rejectButton.click(); + + fixture.detectChanges(); + expect(component.accountRequests[0].status).toEqual(AccountRequestStatus.REJECTED); + }); +}); diff --git a/src/web/app/components/account-requests-table/account-request-table.component.ts b/src/web/app/components/account-requests-table/account-request-table.component.ts new file mode 100755 index 00000000000..ce51e6ef3c7 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.component.ts @@ -0,0 +1,195 @@ +import { Component, Input } from '@angular/core'; +import { NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AccountRequestTableRowModel } from './account-request-table-model'; +import { EditRequestModalComponent } from './admin-edit-request-modal/admin-edit-request-modal.component'; +import { + RejectWithReasonModalComponent, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component'; +import { AccountService } from '../../../services/account.service'; +import { SimpleModalService } from '../../../services/simple-modal.service'; +import { StatusMessageService } from '../../../services/status-message.service'; +import { AccountRequest, MessageOutput } from '../../../types/api-output'; +import { ErrorMessageOutput } from '../../error-message-output'; +import { SimpleModalType } from '../simple-modal/simple-modal-type'; +import { collapseAnim } from '../teammates-common/collapse-anim'; + +/** + * Account requests table component. + */ +@Component({ + selector: 'tm-account-request-table', + templateUrl: './account-request-table.component.html', + styleUrls: ['./account-request-table.component.scss'], + animations: [collapseAnim], +}) + +export class AccountRequestTableComponent { + + @Input() + accountRequests: AccountRequestTableRowModel[] = []; + + @Input() + searchString = ''; + + constructor( + private statusMessageService: StatusMessageService, + private simpleModalService: SimpleModalService, + private accountService: AccountService, + private ngbModal: NgbModal, + ) {} + + /** + * Shows all account requests' links in the page. + */ + showAllAccountRequestsLinks(): void { + for (const accountRequest of this.accountRequests) { + accountRequest.showLinks = true; + } + } + + /** + * Hides all account requests' links in the page. + */ + hideAllAccountRequestsLinks(): void { + for (const accountRequest of this.accountRequests) { + accountRequest.showLinks = false; + } + } + + editAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalRef: NgbModalRef = this.ngbModal.open(EditRequestModalComponent); + modalRef.componentInstance.accountRequestName = accountRequest.name; + modalRef.componentInstance.accountRequestEmail = accountRequest.email; + modalRef.componentInstance.accountRequestInstitution = accountRequest.instituteAndCountry; + modalRef.componentInstance.accountRequestComments = accountRequest.comments; + + modalRef.result.then(() => { + this.accountService.editAccountRequest( + accountRequest.id, + modalRef.componentInstance.accountRequestName, + modalRef.componentInstance.accountRequestEmail, + modalRef.componentInstance.accountRequestInstitution, + accountRequest.status, + modalRef.componentInstance.accountRequestComments) + .subscribe({ + next: (resp: AccountRequest) => { + accountRequest.comments = resp.comments ?? ''; + accountRequest.name = resp.name; + accountRequest.email = resp.email; + accountRequest.instituteAndCountry = resp.institute; + this.statusMessageService.showSuccessToast('Account request was successfully updated.'); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }); + } + + approveAccountRequest(accountRequest: AccountRequestTableRowModel): void { + this.accountService.approveAccountRequest(accountRequest.id, accountRequest.name, + accountRequest.email, accountRequest.instituteAndCountry) + .subscribe({ + next: (resp : AccountRequest) => { + accountRequest.status = resp.status; + this.statusMessageService.showSuccessToast( + `Account request was successfully approved. Email has been sent to ${accountRequest.email}.`, + ); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + } + + resetAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent = `Are you sure you want to reset the account request for + ${accountRequest.name} with email ${accountRequest.email} from + ${accountRequest.instituteAndCountry}? + An email with the account registration link will also be sent to the instructor.`; + const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( + `Reset account request for ${accountRequest.name}?`, SimpleModalType.WARNING, modalContent); + + modalRef.result.then(() => { + this.accountService.resetAccountRequest(accountRequest.id) + .subscribe({ + next: () => { + this.statusMessageService + .showSuccessToast(`Reset successful. An email has been sent to ${accountRequest.email}.`); + accountRequest.registeredAtText = ''; + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + deleteAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent: string = `Are you sure you want to delete the account request for + ${accountRequest.name} with email ${accountRequest.email} from + ${accountRequest.instituteAndCountry}?`; + const modalRef: NgbModalRef = this.simpleModalService.openConfirmationModal( + `Delete account request for ${accountRequest.name}?`, SimpleModalType.DANGER, modalContent); + + modalRef.result.then(() => { + this.accountService.deleteAccountRequest(accountRequest.id) + .subscribe({ + next: (resp: MessageOutput) => { + this.statusMessageService.showSuccessToast(resp.message); + this.accountRequests = this.accountRequests.filter((x: AccountRequestTableRowModel) => x !== accountRequest); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + viewAccountRequest(accountRequest: AccountRequestTableRowModel): void { + const modalContent: string = `Comment: ${accountRequest.comments || 'No comments'}`; + const modalRef: NgbModalRef = this.simpleModalService.openInformationModal( + `Comments for ${accountRequest.name} Request`, SimpleModalType.INFO, modalContent); + + modalRef.result.then(() => {}, () => {}); + } + + rejectAccountRequest(accountRequest: AccountRequestTableRowModel): void { + this.accountService.rejectAccountRequest(accountRequest.id) + .subscribe({ + next: (resp : AccountRequest) => { + accountRequest.status = resp.status; + this.statusMessageService.showSuccessToast('Account request was successfully rejected.'); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + } + + rejectAccountRequestWithReason(accountRequest: AccountRequestTableRowModel): void { + const modalRef: NgbModalRef = this.ngbModal.open(RejectWithReasonModalComponent); + modalRef.componentInstance.accountRequestName = accountRequest.name; + modalRef.componentInstance.accountRequestEmail = accountRequest.email; + + modalRef.result.then(() => { + this.accountService.rejectAccountRequest(accountRequest.id, + modalRef.componentInstance.rejectionReasonTitle, modalRef.componentInstance.rejectionReasonBody) + .subscribe({ + next: (resp: AccountRequest) => { + accountRequest.status = resp.status; + this.statusMessageService.showSuccessToast( + `Account request was successfully rejected. Email has been sent to ${accountRequest.email}.`, + ); + }, + error: (resp: ErrorMessageOutput) => { + this.statusMessageService.showErrorToast(resp.error.message); + }, + }); + }, () => {}); + } + + trackAccountRequest(accountRequest: AccountRequestTableRowModel): string { + return accountRequest.id; + } +} diff --git a/src/web/app/components/account-requests-table/account-request-table.module.ts b/src/web/app/components/account-requests-table/account-request-table.module.ts new file mode 100644 index 00000000000..2ff431b1021 --- /dev/null +++ b/src/web/app/components/account-requests-table/account-request-table.module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbTooltipModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { AccountRequestTableComponent } from './account-request-table.component'; +import { EditRequestModalComponent } from './admin-edit-request-modal/admin-edit-request-modal.component'; +import { + RejectWithReasonModalComponent, +} from './admin-reject-with-reason-modal/admin-reject-with-reason-modal.component'; +import { Pipes } from '../../pipes/pipes.module'; +import { RichTextEditorModule } from '../rich-text-editor/rich-text-editor.module'; + +/** + * Module for account requests table. + */ +@NgModule({ + declarations: [ + AccountRequestTableComponent, + EditRequestModalComponent, + RejectWithReasonModalComponent, + ], + exports: [ + AccountRequestTableComponent, + ], + imports: [ + CommonModule, + FormsModule, + NgbTooltipModule, + NgbDropdownModule, + Pipes, + RichTextEditorModule, + ], +}) +export class AccountRequestTableModule { } diff --git a/src/web/app/components/account-requests-table/admin-edit-request-modal/__snapshots__/admin-edit-request-modal.component.spec.ts.snap b/src/web/app/components/account-requests-table/admin-edit-request-modal/__snapshots__/admin-edit-request-modal.component.spec.ts.snap new file mode 100644 index 00000000000..17770c36eb2 --- /dev/null +++ b/src/web/app/components/account-requests-table/admin-edit-request-modal/__snapshots__/admin-edit-request-modal.component.spec.ts.snap @@ -0,0 +1,215 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RejectWithReasonModal should show empty fields 1`] = ` + +