fix(manager-api-notifications): fix JPQL for marking notifications as read
test(manager-api-notifications): add tests for getting user's notifications
MODIFY
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/NotificationCriteriaBean.java
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/NotificationCriteriaBean.java
@@ -18,7 +18,7 @@ public class NotificationCriteriaBean extends SearchCriteriaBean {
"category",
"reason",
"reasonMessage",
- "notificationStatus",
+ "status",
"createdOn",
"modifiedOn",
"source"

MODIFY
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/NotificationEntity.java
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/NotificationEntity.java
@@ -56,9 +56,9 @@ public class NotificationEntity {
private String reasonMessage;
@Enumerated(EnumType.STRING)
- @Column(name = "notification_status", nullable = false)
+ @Column(name = "status", nullable = false)
@NotNull
- private NotificationStatus notificationStatus;
+ private NotificationStatus status;
// TODO(msavy): consider tracking dismissal reason? for example, old or irrelevant, etc?
@CreationTimestamp
@@ -121,13 +121,12 @@ public class NotificationEntity {
return this;
}
- public NotificationStatus getNotificationStatus() {
- return notificationStatus;
+ public NotificationStatus getStatus() {
+ return status;
}
- public NotificationEntity setNotificationStatus(
- NotificationStatus notificationStatus) {
- this.notificationStatus = notificationStatus;
+ public NotificationEntity setStatus(NotificationStatus status) {
+ this.status = status;
return this;
}
@@ -213,7 +212,7 @@ public class NotificationEntity {
.add("category=" + category)
.add("reason='" + reason + "'")
.add("reasonMessage='" + reasonMessage + "'")
- .add("notificationStatus=" + notificationStatus)
+ .add("status=" + status)
.add("createdOn=" + createdOn)
.add("modifiedOn=" + modifiedOn)
.add("recipient='" + recipient + "'")

MODIFY
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/dto/NotificationActionDto.java
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/dto/NotificationActionDto.java
@@ -5,9 +5,13 @@ import io.apiman.manager.api.beans.notifications.NotificationStatus;
import java.util.List;
import javax.validation.constraints.NotNull;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
/**
* @author Marc Savy {@literal <marc@blackparrotlabs.io>}
*/
+@JsonInclude(Include.NON_NULL)
public class NotificationActionDto {
private boolean markAll = false;
private List<Long> notificationIds;

MODIFY
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/dto/NotificationDto.java
manager/api/beans/src/main/java/io/apiman/manager/api/beans/notifications/dto/NotificationDto.java
@@ -8,8 +8,6 @@ import io.apiman.manager.api.beans.notifications.NotificationStatus;
import java.time.OffsetDateTime;
import java.util.StringJoiner;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-
/**
* @author Marc Savy {@literal <marc@blackparrotlabs.io>}
*/
@@ -18,7 +16,7 @@ public class NotificationDto<P extends IVersionedApimanEvent> {
private NotificationCategory category;
private String reason;
private String reasonMessage;
- private NotificationStatus notificationStatus;
+ private NotificationStatus status;
private OffsetDateTime createdOn;
private OffsetDateTime modifiedOn;
private UserDto recipient;
@@ -64,13 +62,13 @@ public class NotificationDto<P extends IVersionedApimanEvent> {
return this;
}
- public NotificationStatus getNotificationStatus() {
- return notificationStatus;
+ public NotificationStatus getStatus() {
+ return status;
}
- public NotificationDto<P> setNotificationStatus(
- NotificationStatus notificationStatus) {
- this.notificationStatus = notificationStatus;
+ public NotificationDto<P> setStatus(
+ NotificationStatus status) {
+ this.status = status;
return this;
}
@@ -126,7 +124,7 @@ public class NotificationDto<P extends IVersionedApimanEvent> {
.add("category=" + category)
.add("reason='" + reason + "'")
.add("reasonMessage='" + reasonMessage + "'")
- .add("notificationStatus=" + notificationStatus)
+ .add("notificationStatus=" + status)
.add("createdOn=" + createdOn)
.add("modifiedOn=" + modifiedOn)
.add("recipient='" + recipient + "'")

MODIFY
manager/api/jpa/src/main/java/io/apiman/manager/api/jpa/NotificationRepositoryImpl.java
manager/api/jpa/src/main/java/io/apiman/manager/api/jpa/NotificationRepositoryImpl.java
@@ -129,7 +129,7 @@ public class NotificationRepositoryImpl extends AbstractJpaStorage implements IN
int n = getActiveEntityManager()
.createQuery(
"UPDATE NotificationEntity n "
- + "SET n.notificationStatus = :newStatus, "
+ + "SET n.status = :newStatus, "
+ " n.modifiedOn = :now "
+ "WHERE n.recipient = :recipientId "
+ "AND n.id IN :idList"
@@ -148,9 +148,9 @@ public class NotificationRepositoryImpl extends AbstractJpaStorage implements IN
int n = getActiveEntityManager()
.createQuery(
"UPDATE NotificationEntity n "
- + "SET n.notificationStatus = :newStatus, "
+ + "SET n.status = :newStatus, "
+ " n.modifiedOn = :now "
- + "WHERE n.recipient = :recipientId AND n.notificationStatus = 'OPEN'"
+ + "WHERE n.recipient = :recipientId AND n.status = 'OPEN'"
)
.setParameter("newStatus", status)
.setParameter("recipientId", recipientUserId)

MODIFY
manager/api/rest-impl/src/main/java/io/apiman/manager/api/rest/impl/UserResourceImpl.java
manager/api/rest-impl/src/main/java/io/apiman/manager/api/rest/impl/UserResourceImpl.java
@@ -230,8 +230,7 @@ public class UserResourceImpl implements IUserResource, DataAccessUtilMixin {
* {@inheritDoc}
*/
@Override
- public SearchResultsBean<NotificationDto<?>> getNotificationsForUser(String userId,
- NotificationCriteriaBean criteria)
+ public SearchResultsBean<NotificationDto<?>> getNotificationsForUser(String userId, NotificationCriteriaBean criteria)
throws UserNotFoundException, NotAuthorizedException {
securityContext.checkIfUserIsCurrentUser(userId);
SearchCriteriaUtil.validateSearchCriteria(criteria);
@@ -249,6 +248,9 @@ public class UserResourceImpl implements IUserResource, DataAccessUtilMixin {
return Response.noContent().header("X-Total-Count", notificationCount).build();
}
+ /**
+ * {@inheritDoc}
+ */
@Override
public Response markNotifications(String userId, NotificationActionDto notificationAction)
throws UserNotFoundException, NotAuthorizedException {
@@ -260,7 +262,7 @@ public class UserResourceImpl implements IUserResource, DataAccessUtilMixin {
} else {
notificationService.markNotificationsWithStatus(userId, notificationAction.getNotificationIds(), notificationAction.getStatus());
}
- return Response.accepted().build();
+ return Response.noContent().build();
}
/**

MODIFY
manager/api/rest-impl/src/main/java/io/apiman/manager/api/service/NotificationService.java
manager/api/rest-impl/src/main/java/io/apiman/manager/api/service/NotificationService.java
@@ -172,7 +172,7 @@ public class NotificationService implements DataAccessUtilMixin {
.setCategory(newNotification.getCategory())
.setReason(newNotification.getReason())
.setReasonMessage(newNotification.getReasonMessage())
- .setNotificationStatus(NotificationStatus.OPEN)
+ .setStatus(NotificationStatus.OPEN)
.setRecipient(resolvedRecipient.getUsername())
.setSource(newNotification.getSource())
.setPayload(JsonUtil.toJsonTree(newNotification.getPayload()));
@@ -271,7 +271,7 @@ public class NotificationService implements DataAccessUtilMixin {
.setCategory(newNotification.getCategory())
.setReason(newNotification.getReason())
.setReasonMessage(newNotification.getReasonMessage())
- .setNotificationStatus(NotificationStatus.OPEN)
+ .setStatus(NotificationStatus.OPEN)
.setCreatedOn(newNotification.getCreatedOn())
.setModifiedOn(newNotification.getModifiedOn())
.setRecipient(user)

MODIFY
manager/api/rest/src/main/java/io/apiman/manager/api/rest/IUserResource.java
manager/api/rest/src/main/java/io/apiman/manager/api/rest/IUserResource.java
@@ -249,13 +249,37 @@ public interface IUserResource {
public SearchResultsBean<NotificationDto<?>> getNotificationsForUser(@PathParam("userId") String userId, NotificationCriteriaBean criteria)
throws UserNotFoundException, NotAuthorizedException;
+ /**
+ * Get the number of notifications for a given user. Inspect the <sample>X-Total-Count</sample> header.
+ *
+ * <p>Users are only able to get information about their own notifications.
+ *
+ * @param userId the user ID.
+ * @param unread whether to only count unread notifications (default: true).
+ * @return X-Total-Count header with the number of unread notifications.
+ * @throws UserNotFoundException when a request is sent for a user who does not exist
+ * @throws NotAuthorizedException when the user attempts to do or see something that they are not authorized (do not have permission) to
+ */
@HEAD
@Path("{userId}/notifications")
public Response getNotificationCountForUser(@PathParam("userId") String userId, @DefaultValue("true") @QueryParam("unread") boolean unread)
throws UserNotFoundException, NotAuthorizedException;
- @POST
+ /**
+ * Mark a user's notifications with a specific state by ID (or all, if desired).
+ *
+ * <p>Users are only able to operate on their own notifications. Any attempt to operate on unowned
+ * notifications will either be rejected or silently ignored.
+ *
+ * @param userId the user ID.
+ * @param notificationAction the action to take on the notifications.
+ * @return X-Total-Count header with the number of unread notifications.
+ * @throws UserNotFoundException when a request is sent for a user who does not exist
+ * @throws NotAuthorizedException when the user attempts to do or see something that they are not authorized (do not have permission) to
+ */
+ @PUT
@Path("{userId}/notifications")
+ @Consumes(MediaType.APPLICATION_JSON)
public Response markNotifications(@PathParam("userId") String userId, @NotNull NotificationActionDto notificationAction)
throws UserNotFoundException, NotAuthorizedException;
}

MODIFY
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/001_approver-notifications.resttest
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/001_approver-notifications.resttest
@@ -16,7 +16,7 @@ Content-Type: application/json
"category": "API_ADMINISTRATION",
"reason": "apiman.client.contract.approval.request",
"reasonMessage": "Signup request for API",
- "notificationStatus": "OPEN",
+ "status": "OPEN",
"recipient": {
"id": "admin",
"username": "admin",
@@ -52,14 +52,14 @@ Content-Type: application/json
"category": "API_ADMINISTRATION",
"reason": "apiman.client.contract.approval.request",
"reasonMessage": "Signup request for API",
- "notificationStatus": "OPEN",
+ "status": "OPEN",
"recipient": {
"id": "admin",
"username": "admin",
"fullName": "Admin",
"email": "admin@example.org"
},
- "source": "/action/approval-request",
+ "source": "http://somepage/here/",
"payload": {
"headers": {
"source": "/a/b/c",

MODIFY
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/002_bwayne-notifications.resttest
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/002_bwayne-notifications.resttest
@@ -16,7 +16,7 @@ Content-Type: application/json
"category": "API_ADMINISTRATION",
"reason": "apiman.client.status_change",
"reasonMessage": "Client was published",
- "notificationStatus": "OPEN",
+ "status": "OPEN",
"recipient": {
"id": "bwayne",
"username": "bwayne",
@@ -42,7 +42,7 @@ Content-Type: application/json
"category": "API_ADMINISTRATION",
"reason": "apiman.client.contract.approval.granted",
"reasonMessage": "Signup was approved!",
- "notificationStatus": "OPEN",
+ "status": "OPEN",
"recipient": {
"id": "bwayne",
"username": "bwayne",
@@ -79,7 +79,7 @@ Content-Type: application/json
"category": "API_ADMINISTRATION",
"reason": "apiman.client.contract.approval.granted",
"reasonMessage": "Signup was approved!",
- "notificationStatus": "OPEN",
+ "status": "OPEN",
"recipient": {
"id": "bwayne",
"username": "bwayne",

ADD
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/003_bwayne-mark-all-read.resttest
+PUT /users/bwayne/notifications bwayne/bwayne
+Content-Type: application/json
+
+{
+ "markAll": true,
+ "status": "USER_DISMISSED"
+}
+----
+204

ADD
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/004_bwayne-list-notifications-all-read.resttest
+POST /users/bwayne/notifications bwayne/bwayne
+Content-Type: application/json
+
+{
+ "orderBy" : {
+ "name" : "id",
+ "ascending" : false
+ }
+}
+----
+200
+Content-Type: application/json
+
+{
+ "beans": [{
+ "category": "API_ADMINISTRATION",
+ "reason": "apiman.client.status_change",
+ "reasonMessage": "Client was published",
+ "status": "USER_DISMISSED"
+ }, {
+ "category": "API_ADMINISTRATION",
+ "reason": "apiman.client.contract.approval.granted",
+ "reasonMessage": "Signup was approved!",
+ "status": "USER_DISMISSED"
+ }, {
+ "category": "API_ADMINISTRATION",
+ "reason": "apiman.client.contract.approval.granted",
+ "reasonMessage": "Signup was approved!",
+ "status": "USER_DISMISSED"
+ }],
+ "totalSize": 3
+}

ADD
manager/test/api/src/test/resources/test-plan-data/approvals/notifications/005_bwayne-list-unread-notifications-empty.resttest
+POST /users/bwayne/notifications bwayne/bwayne
+Content-Type: application/json
+
+{
+ "orderBy" : {
+ "name" : "id",
+ "ascending" : false
+ },
+ "filters": [{
+ "name": "status",
+ "operator": "eq",
+ "value": "OPEN"
+ }]
+}
+----
+200
+Content-Type: application/json
+
+{
+ "beans": [],
+ "totalSize": 0
+}

MODIFY
manager/test/api/src/test/resources/test-plans/approvals-testPlan.xml
manager/test/api/src/test/resources/test-plans/approvals-testPlan.xml
<test name="Approver receives notification about contract signup request">test-plan-data/approvals/notifications/001_approver-notifications.resttest</test>
<test name="User bwayne receives notifications about approvals">test-plan-data/approvals/notifications/002_bwayne-notifications.resttest</test>
+
+ <test name="bwayne mark all notifications read">test-plan-data/approvals/notifications/003_bwayne-mark-all-read.resttest</test>
+ <test name="bwayne list notifications (all read)">test-plan-data/approvals/notifications/004_bwayne-list-notifications-all-read.resttest</test>
+ <test name="bwayne list unread notifications (empty)">test-plan-data/approvals/notifications/005_bwayne-list-unread-notifications-empty.resttest</test>
+
+
</testGroup>
</testPlan>

fix(manager-api-notifications): fix JPQL for marking notifications as read 

test(manager-api-notifications): add tests for getting user's notifications