Skip to content

Commit

Permalink
[#11878] Create reject account request endpoint (#12985)
Browse files Browse the repository at this point in the history
* Create account request rejection endpoint

* Add validation

* Add check for already rejected request when sending email

* Add integration test cases

* Set request method to post

* Fix lint errors

* Update tests list

* Update validation check

* Add test for validation

* Fix lint errors

* Fix validation comparison

* Fix error message test

* Add email sending

* Update test cases

* Refactor reason check code for clarity
  • Loading branch information
xenosf committed Apr 9, 2024
1 parent 96e5abd commit fb0ba19
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 3 deletions.
208 changes: 208 additions & 0 deletions src/it/java/teammates/it/ui/webapi/RejectAccountRequestActionIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package teammates.it.ui.webapi;

import java.util.UUID;

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.storage.sqlentity.Course;
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<RejectAccountRequestAction> {

private static final String TYPICAL_TITLE = "We are Unable to Create an Account for you";
private static final String TYPICAL_BODY = new StringBuilder()
.append("<p>Hi, Example</p>\n")
.append("<p>Thanks for your interest in using TEAMMATES. ")
.append("We are unable to create a TEAMMATES instructor account for you.</p>\n\n")
.append("<p>\n")
.append(" <strong>Reason:</strong> The email address you provided ")
.append("is not an 'official' email address provided by your institution.<br />\n")
.append(" <strong>Remedy:</strong> ")
.append("Please re-submit an account request with your 'official' institution email address.\n")
.append("</p>\n\n")
.append("<p>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.</p>\n")
.append("<p>Regards,<br />TEAMMATES Team.</p>\n")
.toString();

@Override
@BeforeMethod
protected void setUp() throws Exception {
super.setUp();
persistDataBundle(typicalBundle);
HibernateUtil.flushSession();
}

@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 {
AccountRequest accountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1");
accountRequest.setStatus(AccountRequestStatus.PENDING);
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 {
AccountRequest accountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1");
accountRequest.setStatus(AccountRequestStatus.PENDING);
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() {
AccountRequest accountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1");
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() {
AccountRequest accountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1");
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 {
AccountRequest accountRequest = typicalBundle.accountRequests.get("unregisteredInstructor1");
accountRequest.setStatus(AccountRequestStatus.REJECTED);
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() {
AccountRequestRejectionRequest requestBody = new AccountRequestRejectionRequest(null, null);
String[] params = new String[] {Const.ParamsNames.ACCOUNT_REQUEST_ID, "invalid"};

InvalidHttpParameterException ihpe = verifyHttpParameterFailure(requestBody, params);
assertEquals("Invalid UUID string: 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 {
Course course = typicalBundle.courses.get("course1");
verifyOnlyAdminCanAccess(course);
}
}
3 changes: 3 additions & 0 deletions src/main/java/teammates/common/util/Const.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -337,6 +339,7 @@ public static class ResourceURIs {
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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum ResourceEndpoints {
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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/main/java/teammates/ui/webapi/ActionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public final class ActionFactory {
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);
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/teammates/ui/webapi/RejectAccountRequestAction.java
Original file line number Diff line number Diff line change
@@ -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 JsonResult execute() throws InvalidOperationException, InvalidHttpRequestBodyException {
String id = getNonNullRequestParamValue(Const.ParamsNames.ACCOUNT_REQUEST_ID);
UUID accountRequestId;

try {
accountRequestId = UUID.fromString(id);
} catch (IllegalArgumentException e) {
throw new InvalidHttpParameterException(e.getMessage(), e);
}

AccountRequest accountRequest = sqlLogic.getAccountRequest(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.updateAccountRequest(accountRequest);
if (accountRequestRejectionRequest.checkHasReason()
&& initialStatus != AccountRequestStatus.REJECTED) {
EmailWrapper email = sqlEmailGenerator.generateAccountRequestRejectionEmail(accountRequest,
accountRequestRejectionRequest.getReasonTitle(), accountRequestRejectionRequest.getReasonBody());
emailSender.sendEmail(email);
}
} catch (InvalidParametersException e) {
throw new InvalidHttpRequestBodyException(e);
} catch (EntityDoesNotExistException e) {
throw new EntityNotFoundException(e);
}

return new JsonResult(new AccountRequestData(accountRequest));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
*/
public class UpdateAccountRequestAction extends AdminOnlyAction {

static final String ACCOUNT_REQUEST_NOT_FOUND = "Account request with id = %s not found";

@Override
public boolean isTransactionNeeded() {
return false;
Expand All @@ -38,7 +36,7 @@ public JsonResult execute() throws InvalidOperationException, InvalidHttpRequest
AccountRequest accountRequest = sqlLogic.getAccountRequestWithTransaction(accountRequestId);

if (accountRequest == null) {
String errorMessage = String.format(ACCOUNT_REQUEST_NOT_FOUND, accountRequestId.toString());
String errorMessage = String.format(Const.ACCOUNT_REQUEST_NOT_FOUND, accountRequestId.toString());
throw new EntityNotFoundException(errorMessage);
}

Expand Down
Loading

0 comments on commit fb0ba19

Please sign in to comment.