Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#11878] Create reject account request endpoint #12985

Merged
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 [email protected].</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);
}
Comment on lines +22 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is already a method for this, getUuidRequestParamValue. It would probably be better to use that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xenosf let's submit a separate PR to address this


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
Loading