diff --git a/doc/release-notes/11648-notifications-api-extension.md b/doc/release-notes/11648-notifications-api-extension.md new file mode 100644 index 00000000000..ee5aa22863d --- /dev/null +++ b/doc/release-notes/11648-notifications-api-extension.md @@ -0,0 +1,7 @@ +# getAllNotificationsForUser API extension + +- Extended endpoint getAllNotificationsForUser(``/notifications/all``), which now supports an optional query parameter ``inAppNotificationFormat`` which, if sent as ``true``, retrieves the fields needed to build the in-app notifications for the Notifications section of the Dataverse UI, omitting fields related to email notifications. See also #11648 and #11696. + +# Notifications triggered by API endpoints + +The addDataset and addDataverse API endpoints now trigger user notifications upon successful execution. See also #1342 and #11696. diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 8de396e14b3..04aaf59de98 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -6001,6 +6001,35 @@ The expected OK (200) response looks something like this: } ... +This endpoint supports an optional query parameter ``inAppNotificationFormat`` which, if sent as ``true``, retrieves the fields needed to build the in-app notifications for the Notifications section of the Dataverse UI, omitting fields related to email notifications. + +.. code-block:: bash + + curl -H "X-Dataverse-key:$API_TOKEN" "$SERVER_URL/api/notifications/all?inAppNotificationFormat=true" + +The expected OK (200) response looks something like this: + +.. code-block:: text + + { + "status": "OK", + "data": { + "notifications": [ + { + "id": 79, + "type": "CREATEACC", + "displayAsRead": false, + "sentTimestamp": "2025-08-08T08:00:16Z", + "installationBrandName": "Your Installation Name", + "userGuidesBaseUrl": "https://guides.dataverse.org", + "userGuidesVersion": "6.7.1", + "userGuidesSectionPath": "user/index.html" + } + ] + } + } + ... + Get Unread Count ~~~~~~~~~~~~~~~~ diff --git a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java index 2f727987537..f1099c0a439 100644 --- a/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/PermissionServiceBean.java @@ -13,16 +13,13 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.Command; -import java.util.EnumSet; -import java.util.Map; -import java.util.Set; + +import java.util.*; import java.util.logging.Logger; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; import jakarta.inject.Named; -import java.util.HashSet; -import java.util.List; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import static edu.harvard.iq.dataverse.engine.command.CommandHelper.CH; @@ -34,11 +31,9 @@ import edu.harvard.iq.dataverse.workflow.PendingWorkflowInvocation; import edu.harvard.iq.dataverse.workflow.WorkflowServiceBean; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; import java.util.stream.Collectors; +import java.util.stream.Stream; + import static java.util.stream.Collectors.toList; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; @@ -926,6 +921,7 @@ private boolean hasUnrestrictedReleasedFiles(DatasetVersion targetDatasetVersion public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, Permission permission) { return findPermittedCollections(request, user, 1 << permission.ordinal()); } + public List findPermittedCollections(DataverseRequest request, AuthenticatedUser user, int permissionBit) { if (user != null) { // IP Group - Only check IP if a User is calling for themself @@ -963,5 +959,31 @@ public List findPermittedCollections(DataverseRequest request, Authen } return null; } + + /** + * Calculates the complete list of role assignments for a given user on a DvObject. + * This includes roles assigned directly to the user and roles inherited from any groups + * the user is a member of. + *

+ * This method's logic is based on the private method {@code getRoleStringFromUser} + * in the {@code DataverseUserPage} class, which produces a concatenated string of + * effective user role names required for displaying role-related user notifications. + * The common logic from these two methods may be centralized in the future to + * avoid code duplication. + * + * @param user The authenticated user whose roles are being checked. + * @param dvObject The dataverse object to check for role assignments. + * @return A List containing all effective RoleAssignments for the user. Never null. + */ + public List getEffectiveRoleAssignments(AuthenticatedUser user, DvObject dvObject) { + Stream directAssignments = assignmentsFor(user, dvObject).stream(); + + Stream groupAssignments = groupService.groupsFor(user, dvObject) + .stream() + .flatMap(group -> assignmentsFor(group, dvObject).stream()); + + return Stream.concat(directAssignments, groupAssignments) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java index bec096d9220..ae82ff46522 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Dataverses.java @@ -151,7 +151,7 @@ public Response addDataverse(@Context ContainerRequestContext crc, String body, } AuthenticatedUser u = getRequestAuthenticatedUserOrDie(crc); - newDataverse = execCommand(new CreateDataverseCommand(newDataverse, createDataverseRequest(u), facets, inputLevels, metadataBlocks)); + newDataverse = execCommand(new CreateDataverseCommand(newDataverse, createDataverseRequest(u), facets, inputLevels, metadataBlocks, true)); return created("/dataverses/" + newDataverse.getAlias(), json(newDataverse)); } catch (WrappedResponse ww) { @@ -407,9 +407,9 @@ public Response createDataset(@Context ContainerRequestContext crc, String jsonB ds.setIdentifier(null); ds.setProtocol(null); ds.setGlobalIdCreateTime(null); - Dataset managedDs = null; + Dataset managedDs; try { - managedDs = execCommand(new CreateNewDatasetCommand(ds, createDataverseRequest(u), null, validate)); + managedDs = execCommand(new CreateNewDatasetCommand(ds, createDataverseRequest(u), validate, true)); } catch (WrappedResponse ww) { Throwable cause = ww.getCause(); StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java b/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java index b09da4f5cfb..eebdce5e509 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Notifications.java @@ -1,81 +1,40 @@ package edu.harvard.iq.dataverse.api; -import edu.harvard.iq.dataverse.MailServiceBean; import edu.harvard.iq.dataverse.UserNotification; -import edu.harvard.iq.dataverse.UserNotification.Type; import edu.harvard.iq.dataverse.api.auth.AuthRequired; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.workflows.WorkflowUtil; + import java.util.List; import java.util.Optional; import java.util.Set; -import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.*; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; -import edu.harvard.iq.dataverse.util.MailUtil; -import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.json; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; @Stateless @Path("notifications") public class Notifications extends AbstractApiBean { - @EJB - MailServiceBean mailService; - @GET @AuthRequired @Path("/all") - public Response getAllNotificationsForUser(@Context ContainerRequestContext crc) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); - } - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder(); - List notifications = userNotificationSvc.findByUser(authenticatedUser.getId()); - for (UserNotification notification : notifications) { - NullSafeJsonBuilder notificationObjectBuilder = jsonObjectBuilder(); - JsonArrayBuilder reasonsForReturn = Json.createArrayBuilder(); - Type type = notification.getType(); - notificationObjectBuilder.add("id", notification.getId()); - notificationObjectBuilder.add("type", type.toString()); - notificationObjectBuilder.add("displayAsRead", notification.isReadNotification()); - /* FIXME - Re-add reasons for return if/when they are added to the notifications page. - if (Type.RETURNEDDS.equals(type) || Type.SUBMITTEDDS.equals(type)) { - JsonArrayBuilder reasons = getReasonsForReturn(notification); - for (JsonValue reason : reasons.build()) { - reasonsForReturn.add(reason); - } - notificationObjectBuilder.add("reasonsForReturn", reasonsForReturn); - } - */ - Object objectOfNotification = mailService.getObjectOfNotification(notification); - if (objectOfNotification != null){ - String subjectText = MailUtil.getSubjectTextBasedOnNotification(notification, objectOfNotification); - String messageText = mailService.getMessageTextBasedOnNotification(notification, objectOfNotification, null, notification.getRequestor()); - notificationObjectBuilder.add("subjectText", subjectText); - notificationObjectBuilder.add("messageText", messageText); - } - notificationObjectBuilder.add("sentTimestamp", notification.getSendDateTimestamp()); - jsonArrayBuilder.add(notificationObjectBuilder); + public Response getAllNotificationsForUser(@Context ContainerRequestContext crc, @QueryParam("inAppNotificationFormat") boolean inAppNotificationFormat) { + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + List userNotifications = userNotificationSvc.findByUser(authenticatedUser.getId()); + return ok(Json.createObjectBuilder().add("notifications", json(userNotifications, authenticatedUser, inAppNotificationFormat))); + } catch (WrappedResponse wr) { + return wr.getResponse(); } - JsonObjectBuilder result = Json.createObjectBuilder().add("notifications", jsonArrayBuilder); - return ok(result); } @GET @@ -92,11 +51,6 @@ public Response getUnreadNotificationsCountForUser(@Context ContainerRequestCont } } - private JsonArrayBuilder getReasonsForReturn(UserNotification notification) { - Long objectId = notification.getObjectId(); - return WorkflowUtil.getAllWorkflowComments(datasetVersionSvc.find(objectId)); - } - @PUT @AuthRequired @Path("/{id}/markAsRead") @@ -124,155 +78,141 @@ public Response markNotificationAsReadForUser(@Context ContainerRequestContext c @AuthRequired @Path("/{id}") public Response deleteNotificationForUser(@Context ContainerRequestContext crc, @PathParam("id") long id) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); - } + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + Long userId = authenticatedUser.getId(); + Optional notification = userNotificationSvc.findByUser(userId).stream().filter(x -> x.getId().equals(id)).findFirst(); - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - Long userId = authenticatedUser.getId(); - Optional notification = userNotificationSvc.findByUser(userId).stream().filter(x -> x.getId().equals(id)).findFirst(); + if (notification.isPresent()) { + userNotificationSvc.delete(notification.get()); + return ok("Notification " + id + " deleted."); + } - if (notification.isPresent()) { - userNotificationSvc.delete(notification.get()); - return ok("Notification " + id + " deleted."); + return notFound("Notification " + id + " not found."); + } catch (WrappedResponse wr) { + return wr.getResponse(); } - - return notFound("Notification " + id + " not found."); } @GET @AuthRequired @Path("/mutedEmails") public Response getMutedEmailsForUser(@Context ContainerRequestContext crc) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + JsonArrayBuilder mutedEmails = Json.createArrayBuilder(); + authenticatedUser.getMutedEmails().stream().forEach( + x -> mutedEmails.add(jsonObjectBuilder().add("name", x.name()).add("description", x.getDescription())) + ); + JsonObjectBuilder result = Json.createObjectBuilder().add("mutedEmails", mutedEmails); + return ok(result); + } catch (WrappedResponse wr) { + return wr.getResponse(); } - - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - JsonArrayBuilder mutedEmails = Json.createArrayBuilder(); - authenticatedUser.getMutedEmails().stream().forEach( - x -> mutedEmails.add(jsonObjectBuilder().add("name", x.name()).add("description", x.getDescription())) - ); - JsonObjectBuilder result = Json.createObjectBuilder().add("mutedEmails", mutedEmails); - return ok(result); } @PUT @AuthRequired @Path("/mutedEmails/{typeName}") public Response muteEmailsForUser(@Context ContainerRequestContext crc, @PathParam("typeName") String typeName) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); - } - UserNotification.Type notificationType; try { notificationType = UserNotification.Type.valueOf(typeName); } catch (Exception ignore) { return notFound("Notification type " + typeName + " not found."); } - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - Set mutedEmails = authenticatedUser.getMutedEmails(); - mutedEmails.add(notificationType); - authenticatedUser.setMutedEmails(mutedEmails); - authSvc.update(authenticatedUser); - return ok("Notification emails of type " + typeName + " muted."); + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + Set mutedEmails = authenticatedUser.getMutedEmails(); + mutedEmails.add(notificationType); + authenticatedUser.setMutedEmails(mutedEmails); + authSvc.update(authenticatedUser); + return ok("Notification emails of type " + typeName + " muted."); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } } @DELETE @AuthRequired @Path("/mutedEmails/{typeName}") public Response unmuteEmailsForUser(@Context ContainerRequestContext crc, @PathParam("typeName") String typeName) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); - } - UserNotification.Type notificationType; try { notificationType = UserNotification.Type.valueOf(typeName); } catch (Exception ignore) { return notFound("Notification type " + typeName + " not found."); } - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - Set mutedEmails = authenticatedUser.getMutedEmails(); - mutedEmails.remove(notificationType); - authenticatedUser.setMutedEmails(mutedEmails); - authSvc.update(authenticatedUser); - return ok("Notification emails of type " + typeName + " unmuted."); + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + Set mutedEmails = authenticatedUser.getMutedEmails(); + mutedEmails.remove(notificationType); + authenticatedUser.setMutedEmails(mutedEmails); + authSvc.update(authenticatedUser); + return ok("Notification emails of type " + typeName + " unmuted."); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } } @GET @AuthRequired @Path("/mutedNotifications") public Response getMutedNotificationsForUser(@Context ContainerRequestContext crc) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + JsonArrayBuilder mutedNotifications = Json.createArrayBuilder(); + authenticatedUser.getMutedNotifications().stream().forEach( + x -> mutedNotifications.add(jsonObjectBuilder().add("name", x.name()).add("description", x.getDescription())) + ); + JsonObjectBuilder result = Json.createObjectBuilder().add("mutedNotifications", mutedNotifications); + return ok(result); + } catch (WrappedResponse wr) { + return wr.getResponse(); } - - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - JsonArrayBuilder mutedNotifications = Json.createArrayBuilder(); - authenticatedUser.getMutedNotifications().stream().forEach( - x -> mutedNotifications.add(jsonObjectBuilder().add("name", x.name()).add("description", x.getDescription())) - ); - JsonObjectBuilder result = Json.createObjectBuilder().add("mutedNotifications", mutedNotifications); - return ok(result); } @PUT @AuthRequired @Path("/mutedNotifications/{typeName}") public Response muteNotificationsForUser(@Context ContainerRequestContext crc, @PathParam("typeName") String typeName) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); - } - UserNotification.Type notificationType; try { notificationType = UserNotification.Type.valueOf(typeName); } catch (Exception ignore) { return notFound("Notification type " + typeName + " not found."); } - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - Set mutedNotifications = authenticatedUser.getMutedNotifications(); - mutedNotifications.add(notificationType); - authenticatedUser.setMutedNotifications(mutedNotifications); - authSvc.update(authenticatedUser); - return ok("Notification of type " + typeName + " muted."); + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + Set mutedNotifications = authenticatedUser.getMutedNotifications(); + mutedNotifications.add(notificationType); + authenticatedUser.setMutedNotifications(mutedNotifications); + authSvc.update(authenticatedUser); + return ok("Notification of type " + typeName + " muted."); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } } @DELETE @AuthRequired @Path("/mutedNotifications/{typeName}") public Response unmuteNotificationsForUser(@Context ContainerRequestContext crc, @PathParam("typeName") String typeName) { - User user = getRequestUser(crc); - if (!(user instanceof AuthenticatedUser)) { - // It's unlikely we'll reach this error. A Guest doesn't have an API token and would have been blocked above. - return error(Response.Status.BAD_REQUEST, "Only an AuthenticatedUser can have notifications."); - } - UserNotification.Type notificationType; try { notificationType = UserNotification.Type.valueOf(typeName); } catch (Exception ignore) { return notFound("Notification type " + typeName + " not found."); } - AuthenticatedUser authenticatedUser = (AuthenticatedUser) user; - Set mutedNotifications = authenticatedUser.getMutedNotifications(); - mutedNotifications.remove(notificationType); - authenticatedUser.setMutedNotifications(mutedNotifications); - authSvc.update(authenticatedUser); - return ok("Notification of type " + typeName + " unmuted."); + try { + AuthenticatedUser authenticatedUser = getRequestAuthenticatedUserOrDie(crc); + Set mutedNotifications = authenticatedUser.getMutedNotifications(); + mutedNotifications.remove(notificationType); + authenticatedUser.setMutedNotifications(mutedNotifications); + authSvc.update(authenticatedUser); + return ok("Notification of type " + typeName + " unmuted."); + } catch (WrappedResponse wr) { + return wr.getResponse(); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java index 247e5844659..ef46e1b61c3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateDataverseCommand.java @@ -28,19 +28,23 @@ @RequiredPermissions(Permission.AddDataverse) public class CreateDataverseCommand extends AbstractWriteDataverseCommand { + private final boolean sendNotificationOnSuccess; + public CreateDataverseCommand(Dataverse created, DataverseRequest request, List facets, List inputLevels) { - this(created, request, facets, inputLevels, null); + this(created, request, facets, inputLevels, null, false); } public CreateDataverseCommand(Dataverse created, DataverseRequest request, List facets, List inputLevels, - List metadataBlocks) { + List metadataBlocks, + boolean sendNotificationOnSuccess) { super(created, created.getOwner(), request, facets, inputLevels, metadataBlocks); + this.sendNotificationOnSuccess = sendNotificationOnSuccess; } @Override @@ -143,8 +147,30 @@ protected Dataverse innerExecute(CommandContext ctxt) throws IllegalCommandExcep return managedDv; } + /** + * Handles the successful creation of the dataverse by sending a notification + * and triggering indexing. + *

+ * The {@code sendNotificationOnSuccess} flag is used because this command is + * consumed from two different places: the JSF front-end and the API. + *

    + *
  • From JSF: The flag is {@code false}, as the user notification is + * sent separately by the UI logic.
  • + *
  • From the API: The flag is {@code true} to ensure users receive a + * notification when creating a dataverse through the API.
  • + *
+ * + * @param ctxt The command context. + * @param r The created Dataverse object, returned from the command execution. + * @return {@code true} if the dataverse was indexed successfully. + */ @Override public boolean onSuccess(CommandContext ctxt, Object r) { + if (sendNotificationOnSuccess) { + AuthenticatedUser authenticatedUser = (AuthenticatedUser) getUser(); + ctxt.notifications().sendNotification(authenticatedUser, dataverse.getCreateDate(), UserNotification.Type.CREATEDV, dataverse.getId()); + } + return ctxt.dataverses().index((Dataverse) r); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java index c22a2cdb4a2..b1f3147e97f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java +++ b/src/main/java/edu/harvard/iq/dataverse/engine/command/impl/CreateNewDatasetCommand.java @@ -2,13 +2,13 @@ import edu.harvard.iq.dataverse.Dataset; import edu.harvard.iq.dataverse.DatasetVersion; -import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.GlobalId; import edu.harvard.iq.dataverse.RoleAssignment; import edu.harvard.iq.dataverse.Template; import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.engine.command.CommandContext; import edu.harvard.iq.dataverse.engine.command.DataverseRequest; import edu.harvard.iq.dataverse.engine.command.exception.CommandException; @@ -18,10 +18,9 @@ import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import static edu.harvard.iq.dataverse.util.StringUtil.nonEmpty; -import java.util.logging.Logger; import edu.harvard.iq.dataverse.engine.command.RequiredPermissions; -import java.util.List; + import java.sql.Timestamp; import java.time.Instant; @@ -45,10 +44,14 @@ // line above, AND un-comment out the getRequiredPermissions() method below. public class CreateNewDatasetCommand extends AbstractCreateDatasetCommand { - private static final Logger logger = Logger.getLogger(CreateNewDatasetCommand.class.getName()); - + private final Template template; - private final Dataverse dv; + private boolean allowSelfNotification = false; + + public CreateNewDatasetCommand(Dataset theDataset, DataverseRequest aRequest, boolean validate, boolean allowSelfNotification) { + this(theDataset, aRequest, null, validate); + this.allowSelfNotification = allowSelfNotification; + } public CreateNewDatasetCommand(Dataset theDataset, DataverseRequest aRequest) { this( theDataset, aRequest, null); @@ -57,13 +60,11 @@ public CreateNewDatasetCommand(Dataset theDataset, DataverseRequest aRequest) { public CreateNewDatasetCommand(Dataset theDataset, DataverseRequest aRequest, Template template) { super(theDataset, aRequest); this.template = template; - dv = theDataset.getOwner(); } public CreateNewDatasetCommand(Dataset theDataset, DataverseRequest aRequest, Template template, boolean validate) { super(theDataset, aRequest, false, validate); this.template = template; - dv = theDataset.getOwner(); } /** @@ -119,9 +120,9 @@ protected void postPersist( Dataset theDataset, CommandContext ctxt ){ // (saveDataset, that the command returns). This may have been the reason // for the github issue #4783 - where the users were losing their contributor // permissions, when creating datasets AND uploading files in one step. - // In that scenario, an additional UpdateDatasetCommand is exectued on the + // In that scenario, an additional UpdateDatasetCommand is executed on the // dataset returned by the Create command. That issue was fixed by adding - // a full refresh of the datast with datasetService.find() between the + // a full refresh of the dataset with datasetService.find() between the // two commands. But it may be a good idea to make sure they are properly // linked here (?) theDataset.setPermissionModificationTime(getTimestamp()); @@ -131,33 +132,43 @@ protected void postPersist( Dataset theDataset, CommandContext ctxt ){ ctxt.templates().incrementUsageCount(template.getId()); } } - - /* Emails those able to publish the dataset (except the creator themselves who already gets an email) - * that a new dataset exists. - * NB: Needs dataset id so has to be postDBFlush (vs postPersist()) + + /** + * Sends notifications to those able to publish the dataset upon the successful creation of a new dataset. + *

+ * This method checks if dataset creation notifications are enabled. If so, it + * notifies all users with {@code Permission.PublishDataset} on the new dataset. + * The user who initiated the action can be included or excluded from this + * notification based on the allowSelfNotification flag. + * + * @param dataset The newly created {@code Dataset}. + * @param ctxt The {@code CommandContext} providing access to application services. */ - protected void postDBFlush( Dataset theDataset, CommandContext ctxt ){ - if(ctxt.settings().isTrueForKey(SettingsServiceBean.Key.SendNotificationOnDatasetCreation, false)) { - //QDR - alert curators that a dataset has been created - //Should this create a notification too? (which would let us use the notification mailcapbilities to generate the subject/body. - AuthenticatedUser requestor = getUser().isAuthenticated() ? (AuthenticatedUser) getUser() : null; - List authUsers = ctxt.permissions().getUsersWithPermissionOn(Permission.PublishDataset, theDataset); - for (AuthenticatedUser au : authUsers) { - if(!au.equals(requestor)) { - ctxt.notifications().sendNotification( - au, + protected void postDBFlush(Dataset dataset, CommandContext ctxt) { + // 1. Exit early if the SendNotificationOnDatasetCreation setting is disabled. + if (!ctxt.settings().isTrueForKey(SettingsServiceBean.Key.SendNotificationOnDatasetCreation, false)) { + return; + } + + // 2. Identify the user who initiated the action. + final User user = getUser(); + final AuthenticatedUser requestor = user.isAuthenticated() ? (AuthenticatedUser) user : null; + + // 3. Get all users with publish permission and notify them. + ctxt.permissions().getUsersWithPermissionOn(Permission.PublishDataset, dataset) + .stream() + .filter(recipient -> allowSelfNotification || !recipient.equals(requestor)) + .forEach(recipient -> ctxt.notifications().sendNotification( + recipient, Timestamp.from(Instant.now()), UserNotification.Type.DATASETCREATED, - theDataset.getId(), + dataset.getId(), null, requestor, true - ); - } - } - } + )); } - + // Re-enabling the method below will change the permission setup to dynamic. // This will make it so that in an unpublished dataverse only users with the // permission to view it will be allowed to create child datasets. @@ -181,5 +192,5 @@ public Map> getRequiredPermissions() { } return ret; }*/ - + } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index c80e206ec69..69f9262ab5b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -288,9 +288,13 @@ public String getPageURLWithQueryString() { } public String getGuidesBaseUrl() { + return getGuidesBaseUrl(true); + } + + public String getGuidesBaseUrl(boolean includeLang) { String saneDefault = "https://guides.dataverse.org"; String guidesBaseUrl = settingsService.getValueForKey(SettingsServiceBean.Key.GuidesBaseUrl, saneDefault); - return guidesBaseUrl + "/" + getGuidesLanguage(); + return includeLang ? guidesBaseUrl + "/" + getGuidesLanguage() : guidesBaseUrl; } private String getGuidesLanguage() { diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinter.java new file mode 100644 index 00000000000..9a98f3b8413 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinter.java @@ -0,0 +1,263 @@ +package edu.harvard.iq.dataverse.util.json; + +import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.ejb.EJB; +import jakarta.ejb.Stateless; + +import static edu.harvard.iq.dataverse.dataset.DatasetUtil.getLocaleCurationStatusLabel; +import static edu.harvard.iq.dataverse.util.json.JsonPrinter.jsonRoleAssignments; + +/** + * A helper class to build a JSON representation of a UserNotification. + *

+ * It is responsible for adding the correct fields to a JSON object based on the + * notification type. + */ +@Stateless +public class InAppNotificationsJsonPrinter { + + public static final String KEY_ROLE_ASSIGNMENTS = "roleAssignments"; + public static final String KEY_DATAVERSE_ALIAS = "dataverseAlias"; + public static final String KEY_DATAVERSE_DISPLAY_NAME = "dataverseDisplayName"; + public static final String KEY_DATASET_PERSISTENT_ID = "datasetPersistentIdentifier"; + public static final String KEY_DATASET_DISPLAY_NAME = "datasetDisplayName"; + public static final String KEY_OWNER_PERSISTENT_ID = "ownerPersistentIdentifier"; + public static final String KEY_OWNER_ALIAS = "ownerAlias"; + public static final String KEY_OWNER_DISPLAY_NAME = "ownerDisplayName"; + public static final String KEY_REQUESTOR_FIRST_NAME = "requestorFirstName"; + public static final String KEY_REQUESTOR_LAST_NAME = "requestorLastName"; + public static final String KEY_REQUESTOR_EMAIL = "requestorEmail"; + public static final String KEY_DATAFILE_ID = "dataFileId"; + public static final String KEY_DATAFILE_DISPLAY_NAME = "dataFileDisplayName"; + public static final String KEY_GUIDES_BASE_URL = "userGuidesBaseUrl"; + public static final String KEY_GUIDES_VERSION = "userGuidesVersion"; + public static final String KEY_GUIDES_SECTION_PATH = "userGuidesSectionPath"; + public static final String KEY_CURATION_STATUS = "currentCurationStatus"; + public static final String KEY_ADDITIONAL_INFO = "additionalInfo"; + public static final String KEY_OBJECT_DELETED = "objectDeleted"; + public static final String KEY_INSTALLATION_BRAND_NAME = "installationBrandName"; + + public static final String GUIDES_SECTION_PATH_DATAVERSE_MANAGEMENT_HTML = "user/dataverse-management.html"; + public static final String GUIDES_SECTION_PATH_DATASET_MANAGEMENT_HTML = "user/dataset-management.html"; + public static final String GUIDES_SECTION_PATH_DATASET_MANAGEMENT_TABULAR_FILES_HTML = "user/dataset-management.html#tabular-data-files"; + public static final String GUIDES_SECTION_PATH_USER_HTML = "user/index.html"; + + @EJB + private DataverseServiceBean dataverseService; + @EJB + private DatasetServiceBean datasetService; + @EJB + private DatasetVersionServiceBean datasetVersionService; + @EJB + private DataFileServiceBean dataFileService; + @EJB + private PermissionServiceBean permissionService; + @EJB + private SystemConfig systemConfig; + + /** + * Populates a JSON builder with fields specific to the notification type. + * + * @param notificationJson The JSON builder to add fields to. + * @param authenticatedUser The user receiving the notification. + * @param userNotification The notification object containing the details. + */ + public void addFieldsByType(final NullSafeJsonBuilder notificationJson, final AuthenticatedUser authenticatedUser, final UserNotification userNotification) { + final AuthenticatedUser requestor = userNotification.getRequestor(); + + switch (userNotification.getType()) { + case ASSIGNROLE: + case REVOKEROLE: + addRoleFields(notificationJson, authenticatedUser, userNotification); + break; + case CREATEDV: + addCreateDataverseFields(notificationJson, userNotification); + break; + case REQUESTFILEACCESS: + addRequestFileAccessFields(notificationJson, userNotification, requestor); + break; + case REQUESTEDFILEACCESS: + case GRANTFILEACCESS: + case REJECTFILEACCESS: + addDataFileFields(notificationJson, userNotification); + break; + case DATASETCREATED: + addDatasetCreatedFields(notificationJson, userNotification, requestor); + break; + case CREATEDS: + addCreateDatasetFields(notificationJson, userNotification); + break; + case SUBMITTEDDS: + addSubmittedDatasetFields(notificationJson, userNotification, requestor); + break; + case PUBLISHEDDS: + case PUBLISHFAILED_PIDREG: + case RETURNEDDS: + case WORKFLOW_SUCCESS: + case WORKFLOW_FAILURE: + case PIDRECONCILED: + case FILESYSTEMIMPORT: + case CHECKSUMIMPORT: + addDatasetVersionFields(notificationJson, userNotification); + break; + case STATUSUPDATED: + addDatasetVersionFields(notificationJson, userNotification, true); + break; + case CREATEACC: + addCreateAccountFields(notificationJson); + break; + case GLOBUSUPLOADCOMPLETED: + case GLOBUSDOWNLOADCOMPLETED: + case GLOBUSUPLOADCOMPLETEDWITHERRORS: + case GLOBUSUPLOADREMOTEFAILURE: + case GLOBUSUPLOADLOCALFAILURE: + case GLOBUSDOWNLOADCOMPLETEDWITHERRORS: + case CHECKSUMFAIL: + addDatasetFields(notificationJson, userNotification); + break; + case INGESTCOMPLETED: + case INGESTCOMPLETEDWITHERRORS: + addIngestFields(notificationJson, userNotification); + break; + case DATASETMENTIONED: + addDatasetMentionedFields(notificationJson, userNotification); + break; + } + } + + private void addRoleFields(final NullSafeJsonBuilder notificationJson, final AuthenticatedUser authenticatedUser, final UserNotification userNotification) { + Dataverse dataverse = dataverseService.find(userNotification.getObjectId()); + if (dataverse != null) { + notificationJson.add(KEY_ROLE_ASSIGNMENTS, jsonRoleAssignments(permissionService.getEffectiveRoleAssignments(authenticatedUser, dataverse))); + notificationJson.add(KEY_DATAVERSE_ALIAS, dataverse.getAlias()); + notificationJson.add(KEY_DATAVERSE_DISPLAY_NAME, dataverse.getDisplayName()); + } else { + Dataset dataset = datasetService.find(userNotification.getObjectId()); + if (dataset != null) { + notificationJson.add(KEY_ROLE_ASSIGNMENTS, jsonRoleAssignments(permissionService.getEffectiveRoleAssignments(authenticatedUser, dataset))); + notificationJson.add(KEY_DATASET_PERSISTENT_ID, dataset.getGlobalId().asString()); + notificationJson.add(KEY_DATASET_DISPLAY_NAME, dataset.getDisplayName()); + } else { + DataFile datafile = dataFileService.find(userNotification.getObjectId()); + if (datafile != null) { + notificationJson.add(KEY_ROLE_ASSIGNMENTS, jsonRoleAssignments(permissionService.getEffectiveRoleAssignments(authenticatedUser, datafile))); + notificationJson.add(KEY_OWNER_PERSISTENT_ID, datafile.getOwner().getGlobalId().asString()); + notificationJson.add(KEY_OWNER_DISPLAY_NAME, datafile.getOwner().getDisplayName()); + } else { + notificationJson.add(KEY_OBJECT_DELETED, true); + } + } + } + } + + private void addCreateDataverseFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + final Dataverse dataverse = dataverseService.find(userNotification.getObjectId()); + if (dataverse != null) { + notificationJson.add(KEY_DATAVERSE_ALIAS, dataverse.getAlias()); + notificationJson.add(KEY_DATAVERSE_DISPLAY_NAME, dataverse.getDisplayName()); + Dataverse owner = dataverse.getOwner(); + if (owner != null) { + notificationJson.add(KEY_OWNER_ALIAS, owner.getAlias()); + notificationJson.add(KEY_OWNER_DISPLAY_NAME, owner.getDisplayName()); + } + } else { + notificationJson.add(KEY_OBJECT_DELETED, true); + } + addGuidesFields(notificationJson, GUIDES_SECTION_PATH_DATAVERSE_MANAGEMENT_HTML); + } + + private void addCreateAccountFields(final NullSafeJsonBuilder notificationJson) { + notificationJson.add(KEY_INSTALLATION_BRAND_NAME, BrandingUtil.getInstallationBrandName()); + addGuidesFields(notificationJson, GUIDES_SECTION_PATH_USER_HTML); + } + + private void addRequestFileAccessFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification, final AuthenticatedUser requestor) { + addRequestorFields(notificationJson, requestor); + addDataFileFields(notificationJson, userNotification); + } + + private void addDataFileFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + final DataFile dataFile = dataFileService.find(userNotification.getObjectId()); + if (dataFile != null) { + notificationJson.add(KEY_DATAFILE_ID, dataFile.getId()); + notificationJson.add(KEY_DATAFILE_DISPLAY_NAME, dataFile.getDisplayName()); + } else { + notificationJson.add(KEY_OBJECT_DELETED, true); + } + } + + private void addDatasetCreatedFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification, final AuthenticatedUser requestor) { + addDatasetFields(notificationJson, userNotification); + addRequestorFields(notificationJson, requestor); + } + + private void addRequestorFields(final NullSafeJsonBuilder notificationJson, final AuthenticatedUser requestor) { + notificationJson.add(KEY_REQUESTOR_FIRST_NAME, requestor.getFirstName()); + notificationJson.add(KEY_REQUESTOR_LAST_NAME, requestor.getLastName()); + notificationJson.add(KEY_REQUESTOR_EMAIL, requestor.getEmail()); + } + + private void addDatasetFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + final Dataset dataset = datasetService.find(userNotification.getObjectId()); + if (dataset != null) { + notificationJson.add(KEY_DATASET_PERSISTENT_ID, dataset.getGlobalId().asString()); + notificationJson.add(KEY_DATASET_DISPLAY_NAME, dataset.getDisplayName()); + notificationJson.add(KEY_OWNER_ALIAS, dataset.getOwner().getAlias()); + notificationJson.add(KEY_OWNER_DISPLAY_NAME, dataset.getOwner().getDisplayName()); + } else { + notificationJson.add(KEY_OBJECT_DELETED, true); + } + } + + private void addCreateDatasetFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + addGuidesFields(notificationJson, GUIDES_SECTION_PATH_DATASET_MANAGEMENT_HTML); + addDatasetVersionFields(notificationJson, userNotification); + } + + private void addGuidesFields(final NullSafeJsonBuilder notificationJson, String guidesSectionPath) { + notificationJson.add(KEY_GUIDES_BASE_URL, systemConfig.getGuidesBaseUrl(false)); + notificationJson.add(KEY_GUIDES_VERSION, systemConfig.getGuidesVersion()); + + if (guidesSectionPath != null) { + notificationJson.add(KEY_GUIDES_SECTION_PATH, guidesSectionPath); + } + } + + private void addSubmittedDatasetFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification, final AuthenticatedUser requestor) { + addDatasetFields(notificationJson, userNotification); + addRequestorFields(notificationJson, requestor); + } + + private void addDatasetVersionFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + addDatasetVersionFields(notificationJson, userNotification, false); + } + + private void addDatasetVersionFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification, final boolean addCurationStatus) { + final DatasetVersion datasetVersion = datasetVersionService.find(userNotification.getObjectId()); + if (datasetVersion != null) { + Dataset dataset = datasetVersion.getDataset(); + notificationJson.add(KEY_DATASET_PERSISTENT_ID, dataset.getGlobalId().asString()); + notificationJson.add(KEY_DATASET_DISPLAY_NAME, dataset.getDisplayName()); + notificationJson.add(KEY_OWNER_ALIAS, dataset.getOwner().getAlias()); + notificationJson.add(KEY_OWNER_DISPLAY_NAME, dataset.getOwner().getDisplayName()); + if (addCurationStatus) { + notificationJson.add(KEY_CURATION_STATUS, getLocaleCurationStatusLabel(datasetVersion.getCurrentCurationStatus())); + } + } else { + notificationJson.add(KEY_OBJECT_DELETED, true); + } + } + + private void addIngestFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + addDatasetFields(notificationJson, userNotification); + addGuidesFields(notificationJson, GUIDES_SECTION_PATH_DATASET_MANAGEMENT_TABULAR_FILES_HTML); + } + + private void addDatasetMentionedFields(final NullSafeJsonBuilder notificationJson, final UserNotification userNotification) { + addDatasetFields(notificationJson, userNotification); + notificationJson.add(KEY_ADDITIONAL_INFO, userNotification.getAdditionalInfo()); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 592a893083c..1e9ea02f798 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -36,6 +36,7 @@ import edu.harvard.iq.dataverse.util.DatasetFieldWalker; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; +import edu.harvard.iq.dataverse.util.MailUtil; import edu.harvard.iq.dataverse.workflow.Workflow; import edu.harvard.iq.dataverse.workflow.step.WorkflowStepData; @@ -76,11 +77,24 @@ public class JsonPrinter { @EJB static DatasetServiceBean datasetService; + + @EJB + static MailServiceBean mailService; + + @EJB + static InAppNotificationsJsonPrinter inAppNotificationsJsonPrinter; - public static void injectSettingsService(SettingsServiceBean ssb, DatasetFieldServiceBean dfsb, DataverseFieldTypeInputLevelServiceBean dfils, DatasetServiceBean ds) { + public static void injectSettingsService(SettingsServiceBean ssb, + DatasetFieldServiceBean dfsb, + DataverseFieldTypeInputLevelServiceBean dfils, + DatasetServiceBean ds, + MailServiceBean ms, + InAppNotificationsJsonPrinter njp) { settingsService = ssb; datasetFieldService = dfsb; datasetService = ds; + mailService = ms; + inAppNotificationsJsonPrinter = njp; } public JsonPrinter() { @@ -128,11 +142,18 @@ public static JsonObjectBuilder json(AuthenticatedUser authenticatedUser) { return builder; } + public static JsonArrayBuilder jsonRoleAssignments(List roleAssignments) { + JsonArrayBuilder bld = Json.createArrayBuilder(); + roleAssignments.forEach(roleAssignment -> bld.add(json(roleAssignment))); + return bld; + } + public static JsonObjectBuilder json(RoleAssignment ra) { return jsonObjectBuilder() .add("id", ra.getId()) .add("assignee", ra.getAssigneeIdentifier()) .add("roleId", ra.getRole().getId()) + .add("roleName", ra.getRole().getName()) .add("_roleAlias", ra.getRole().getAlias()) .add("privateUrlToken", ra.getPrivateUrlToken()) .add("definitionPointId", ra.getDefinitionPoint().getId()); @@ -1610,4 +1631,37 @@ public static JsonArrayBuilder jsonTemplateInstructions(Map temp return jsonArrayBuilder; } + + public static JsonArrayBuilder json(List notifications, AuthenticatedUser authenticatedUser, boolean inAppNotificationFormat) { + JsonArrayBuilder notificationsArray = Json.createArrayBuilder(); + + for (UserNotification notification : notifications) { + NullSafeJsonBuilder notificationJson = jsonObjectBuilder(); + UserNotification.Type type = notification.getType(); + + notificationJson.add("id", notification.getId()); + notificationJson.add("type", type.toString()); + notificationJson.add("displayAsRead", notification.isReadNotification()); + notificationJson.add("sentTimestamp", notification.getSendDateTimestamp()); + + if (inAppNotificationFormat) { + inAppNotificationsJsonPrinter.addFieldsByType(notificationJson, authenticatedUser, notification); + } else { + Object relatedObject = mailService.getObjectOfNotification(notification); + if (relatedObject != null) { + String subjectText = MailUtil.getSubjectTextBasedOnNotification(notification, relatedObject); + String messageText = mailService.getMessageTextBasedOnNotification( + notification, relatedObject, null, notification.getRequestor() + ); + + notificationJson.add("subjectText", subjectText); + notificationJson.add("messageText", messageText); + } + } + + notificationsArray.add(notificationJson); + } + + return notificationsArray; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinterHelper.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinterHelper.java index aeba4ba797f..a9a05f1b699 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinterHelper.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinterHelper.java @@ -3,6 +3,7 @@ import edu.harvard.iq.dataverse.DatasetFieldServiceBean; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.DataverseFieldTypeInputLevelServiceBean; +import edu.harvard.iq.dataverse.MailServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import jakarta.annotation.PostConstruct; @@ -11,7 +12,7 @@ import jakarta.ejb.Startup; /** - * This is a small helper bean + * This is a small helper bean * As it is a singleton and built at application start (=deployment), it will inject the (stateless) * settings service into the OREMap once it's ready. */ @@ -20,18 +21,31 @@ public class JsonPrinterHelper { @EJB SettingsServiceBean settingsSvc; - + @EJB DatasetFieldServiceBean datasetFieldSvc; - + @EJB DataverseFieldTypeInputLevelServiceBean datasetFieldInpuLevelSvc; @EJB DatasetServiceBean datasetSvc; - + + @EJB + MailServiceBean mailSvc; + + @EJB + InAppNotificationsJsonPrinter inAppNotificationsJsonPrinter; + @PostConstruct public void injectService() { - JsonPrinter.injectSettingsService(settingsSvc, datasetFieldSvc, datasetFieldInpuLevelSvc, datasetSvc); + JsonPrinter.injectSettingsService( + settingsSvc, + datasetFieldSvc, + datasetFieldInpuLevelSvc, + datasetSvc, + mailSvc, + inAppNotificationsJsonPrinter + ); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/InReviewWorkflowIT.java b/src/test/java/edu/harvard/iq/dataverse/api/InReviewWorkflowIT.java index a58b86c2b5a..b327ed41349 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/InReviewWorkflowIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/InReviewWorkflowIT.java @@ -5,9 +5,10 @@ import io.restassured.path.xml.XmlPath; import io.restassured.response.Response; import edu.harvard.iq.dataverse.authorization.DataverseRole; -import java.util.logging.Logger; import jakarta.json.Json; import jakarta.json.JsonObjectBuilder; + +import static edu.harvard.iq.dataverse.UserNotification.Type.*; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.CREATED; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; @@ -21,8 +22,6 @@ public class InReviewWorkflowIT { - private static final Logger logger = Logger.getLogger(DatasetsIT.class.getCanonicalName()); - @BeforeAll public static void setUpClass() { RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); @@ -30,7 +29,7 @@ public static void setUpClass() { } @Test - public void testCuratorSendsCommentsToAuthor() throws InterruptedException { + public void testCuratorSendsCommentsToAuthor() { Response createCurator = UtilIT.createRandomUser(); createCurator.prettyPrint(); createCurator.then().assertThat() @@ -124,7 +123,7 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { Response authorsChecksForCommentsPrematurely = UtilIT.getNotifications(authorApiToken); authorsChecksForCommentsPrematurely.prettyPrint(); authorsChecksForCommentsPrematurely.then().assertThat() - .body("data.notifications[0].type", equalTo("CREATEACC")) + .body("data.notifications[0].type", equalTo(CREATEACC.toString())) // The author thinks, "What's taking the curator so long to review my data?!?" .body("data.notifications[1]", equalTo(null)) .statusCode(OK.getStatusCode()); @@ -136,10 +135,12 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { Response curatorChecksNotificationsAndFindsWorkToDo = UtilIT.getNotifications(curatorApiToken); curatorChecksNotificationsAndFindsWorkToDo.prettyPrint(); curatorChecksNotificationsAndFindsWorkToDo.then().assertThat() - .body("data.notifications[0].type", equalTo("SUBMITTEDDS")) + .body("data.notifications[0].type", equalTo(SUBMITTEDDS.toString())) .body("data.notifications[0].reasonForReturn", equalTo(null)) - .body("data.notifications[1].type", equalTo("CREATEACC")) + .body("data.notifications[1].type", equalTo(CREATEDV.toString())) .body("data.notifications[1].reasonForReturn", equalTo(null)) + .body("data.notifications[2].type", equalTo(CREATEACC.toString())) + .body("data.notifications[2].reasonForReturn", equalTo(null)) .statusCode(OK.getStatusCode()); // Joe Random, a user with no perms on dataset, tries returning the dataset as if he's a curator and fails. @@ -230,12 +231,12 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { Response returnToAuthor = UtilIT.returnDatasetToAuthor(datasetPersistentId, jsonObjectBuilder.build(), curatorApiToken); returnToAuthor.prettyPrint(); } else { - // Increasing the sleep delay here, from 2 to 10 sec.; + // Increasing the sleep delay here, from 2 to 10 sec.; // With the 2 sec. delay, it appears to have been working consistently // on Jenkins (because it's fast, I'm guessing?) - but - // I kept seeing an error on my own build at this point once in a while, - // because the dataset is still locked when we try to edit it, - // a few lines down. -- L.A. Oct. 2018 + // I kept seeing an error on my own build at this point once in a while, + // because the dataset is still locked when we try to edit it, + // a few lines down. -- L.A. Oct. 2018 // Changes to test for ingest lock and 3 seconds duration SEK 09/2019 #6128 assertTrue(UtilIT.sleepForLock(datasetId, "Ingest", curatorApiToken, UtilIT.MAXIMUM_INGEST_LOCK_DURATION), "Failed test if Ingest Lock exceeds max duration " + pathToFileThatGoesThroughIngest); // Thread.sleep(10000); @@ -299,15 +300,15 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { returnToAuthorAlreadyReturned.then().assertThat() .body("message", equalTo("This dataset cannot be return to the author(s) because the latest version is not In Review. The author(s) needs to click Submit for Review first.")) .statusCode(FORBIDDEN.getStatusCode()); - //FIXME when/if reasons for return are returned to notifications page and the API is + //FIXME when/if reasons for return are returned to notifications page and the API is // updated appropriately, these tests will have to be updated. Response authorChecksForCommentsAgain = UtilIT.getNotifications(authorApiToken); authorChecksForCommentsAgain.prettyPrint(); authorChecksForCommentsAgain.then().assertThat() - .body("data.notifications[0].type", equalTo("RETURNEDDS")) + .body("data.notifications[0].type", equalTo(RETURNEDDS.toString())) // The author thinks, "This why we have curators!" //.body("data.notifications[0].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) - .body("data.notifications[1].type", equalTo("CREATEACC")) + .body("data.notifications[1].type", equalTo(CREATEACC.toString())) //.body("data.notifications[1].reasonsForReturn", equalTo(null)) .statusCode(OK.getStatusCode()); @@ -332,13 +333,14 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { curatorChecksNotifications.prettyPrint(); curatorChecksNotifications.then().assertThat() // TODO: Test this issue from the UI as well: https://github.com/IQSS/dataverse/issues/2526 - .body("data.notifications[0].type", equalTo("SUBMITTEDDS")) + .body("data.notifications[0].type", equalTo(SUBMITTEDDS.toString())) //.body("data.notifications[0].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) - .body("data.notifications[1].type", equalTo("INGESTCOMPLETED")) - .body("data.notifications[2].type", equalTo("SUBMITTEDDS")) + .body("data.notifications[1].type", equalTo(INGESTCOMPLETED.toString())) + .body("data.notifications[2].type", equalTo(SUBMITTEDDS.toString())) // Yes, it's a little weird that the first "SUBMITTEDDS" notification now shows the return reason when it showed nothing before. For now we are simply always showing all the reasons for return. They start to stack up. That way you can see the history. //.body("data.notifications[1].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) - .body("data.notifications[3].type", equalTo("CREATEACC")) + .body("data.notifications[3].type", equalTo(CREATEDV.toString())) + .body("data.notifications[4].type", equalTo(CREATEACC.toString())) //.body("data.notifications[2].reasonsForReturn", equalTo(null)) .statusCode(OK.getStatusCode()); @@ -353,14 +355,14 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { Response authorChecksForComments3 = UtilIT.getNotifications(authorApiToken); authorChecksForComments3.prettyPrint(); authorChecksForComments3.then().assertThat() - .body("data.notifications[0].type", equalTo("RETURNEDDS")) + .body("data.notifications[0].type", equalTo(RETURNEDDS.toString())) // .body("data.notifications[0].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) //.body("data.notifications[0].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[1].type", equalTo("RETURNEDDS")) + .body("data.notifications[1].type", equalTo(RETURNEDDS.toString())) // Yes, it's a little weird that the reason for return on the first "RETURNEDDS" changed. We're showing the history. // .body("data.notifications[1].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) // .body("data.notifications[1].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[2].type", equalTo("CREATEACC")) + .body("data.notifications[2].type", equalTo(CREATEACC.toString())) // .body("data.notifications[2].reasonsForReturn", equalTo(null)) .statusCode(OK.getStatusCode()); @@ -384,18 +386,19 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { curatorHopesTheReadmeIsThereNow.prettyPrint(); curatorHopesTheReadmeIsThereNow.then().assertThat() // TODO: Test this issue from the UI as well: https://github.com/IQSS/dataverse/issues/2526 - .body("data.notifications[0].type", equalTo("SUBMITTEDDS")) + .body("data.notifications[0].type", equalTo(SUBMITTEDDS.toString())) // .body("data.notifications[0].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) // .body("data.notifications[0].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[1].type", equalTo("SUBMITTEDDS")) + .body("data.notifications[1].type", equalTo(SUBMITTEDDS.toString())) // .body("data.notifications[1].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) // .body("data.notifications[1].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[2].type", equalTo("INGESTCOMPLETED")) - .body("data.notifications[3].type", equalTo("SUBMITTEDDS")) + .body("data.notifications[2].type", equalTo(INGESTCOMPLETED.toString())) + .body("data.notifications[3].type", equalTo(SUBMITTEDDS.toString())) // Yes, it's a little weird that the first "SUBMITTEDDS" notification now shows the return reason when it showed nothing before. We're showing the history. // .body("data.notifications[2].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) // .body("data.notifications[2].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[4].type", equalTo("CREATEACC")) + .body("data.notifications[4].type", equalTo(CREATEDV.toString())) + .body("data.notifications[5].type", equalTo(CREATEACC.toString())) // .body("data.notifications[3].reasonsForReturn", equalTo(null)) .statusCode(OK.getStatusCode()); @@ -414,15 +417,15 @@ public void testCuratorSendsCommentsToAuthor() throws InterruptedException { Response authorsChecksForCommentsPostPublication = UtilIT.getNotifications(authorApiToken); authorsChecksForCommentsPostPublication.prettyPrint(); authorsChecksForCommentsPostPublication.then().assertThat() - .body("data.notifications[0].type", equalTo("PUBLISHEDDS")) - .body("data.notifications[1].type", equalTo("RETURNEDDS")) + .body("data.notifications[0].type", equalTo(PUBLISHEDDS.toString())) + .body("data.notifications[1].type", equalTo(RETURNEDDS.toString())) // .body("data.notifications[1].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) // .body("data.notifications[1].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[2].type", equalTo("RETURNEDDS")) + .body("data.notifications[2].type", equalTo(RETURNEDDS.toString())) // Yes, it's a little weird that the reason for return on the first "RETURNEDDS" changed. For now we are always showing the most recent reason for return. // .body("data.notifications[2].reasonsForReturn[0].message", equalTo("You forgot to upload any files.")) //.body("data.notifications[2].reasonsForReturn[1].message", equalTo("A README is required.")) - .body("data.notifications[3].type", equalTo("CREATEACC")) + .body("data.notifications[3].type", equalTo(CREATEACC.toString())) // .body("data.notifications[3].reasonsForReturn", equalTo(null)) .statusCode(OK.getStatusCode()); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/NotificationsIT.java b/src/test/java/edu/harvard/iq/dataverse/api/NotificationsIT.java index 5665eef4819..2ccb1365d71 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/NotificationsIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/NotificationsIT.java @@ -1,99 +1,207 @@ package edu.harvard.iq.dataverse.api; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import io.restassured.RestAssured; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; -import java.util.logging.Logger; + +import static edu.harvard.iq.dataverse.UserNotification.Type.*; import static jakarta.ws.rs.core.Response.Status.CREATED; import static jakarta.ws.rs.core.Response.Status.NOT_FOUND; import static jakarta.ws.rs.core.Response.Status.OK; -import static org.hamcrest.CoreMatchers.equalTo; + +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -public class NotificationsIT { +import java.util.Arrays; +import java.util.List; - private static final Logger logger = Logger.getLogger(NotificationsIT.class.getCanonicalName()); +import static org.hamcrest.CoreMatchers.*; +import static org.junit.jupiter.api.Assertions.*; + +public class NotificationsIT { @BeforeAll public static void setUpClass() { RestAssured.baseURI = UtilIT.getRestAssuredBaseUri(); + disableSendNotificationOnDatasetCreationSetting(); + } + + @AfterAll + public static void afterClass() { + disableSendNotificationOnDatasetCreationSetting(); } @Test public void testNotifications() { + // SendNotificationOnDatasetCreation setting is false Response createAuthor = UtilIT.createRandomUser(); - createAuthor.prettyPrint(); createAuthor.then().assertThat() .statusCode(OK.getStatusCode()); - String authorUsername = UtilIT.getUsernameFromResponse(createAuthor); String authorApiToken = UtilIT.getApiTokenFromResponse(createAuthor); - Response nopermsUser = UtilIT.createRandomUser(); - nopermsUser.prettyPrint(); - nopermsUser.then().assertThat() + Response noPermsUser = UtilIT.createRandomUser(); + noPermsUser.then().assertThat() .statusCode(OK.getStatusCode()); - String nopermsApiToken = UtilIT.getApiTokenFromResponse(nopermsUser); + String noPermsApiToken = UtilIT.getApiTokenFromResponse(noPermsUser); - // Some API calls don't generate a notification: https://github.com/IQSS/dataverse/issues/1342 Response createDataverseResponse = UtilIT.createRandomDataverse(authorApiToken); - createDataverseResponse.prettyPrint(); createDataverseResponse.then().assertThat() .statusCode(CREATED.getStatusCode()); String dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); - // Some API calls don't generate a notification: https://github.com/IQSS/dataverse/issues/1342 Response createDataset = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, authorApiToken); - createDataset.prettyPrint(); createDataset.then().assertThat() .statusCode(CREATED.getStatusCode()); Response getNotifications = UtilIT.getNotifications(authorApiToken); - getNotifications.prettyPrint(); getNotifications.then().assertThat() - .body("data.notifications[0].type", equalTo("CREATEACC")) .body("data.notifications[0].displayAsRead", equalTo(false)) - .body("data.notifications[1]", equalTo(null)) + .body("data.notifications[1].displayAsRead", equalTo(false)) + .body("data.notifications.size()", equalTo(2)) .statusCode(OK.getStatusCode()); + String firstNotificationType = JsonPath.from(getNotifications.body().asString()).getString("data.notifications[0].type"); + String secondNotificationType = JsonPath.from(getNotifications.body().asString()).getString("data.notifications[1].type"); + long createAccountId = 0L; + if (firstNotificationType.equals(CREATEDV.toString())) { + assertEquals(CREATEACC.toString(), secondNotificationType); + createAccountId = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[1].id"); + } else if (firstNotificationType.equals(CREATEACC.toString())) { + assertEquals(CREATEDV.toString(), secondNotificationType); + createAccountId = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[0].id"); + } else { + fail("Unexpected notification type: " + firstNotificationType); + } + Response unreadCount = UtilIT.getUnreadNotificationsCount(authorApiToken); - unreadCount.prettyPrint(); unreadCount.then().assertThat() .statusCode(OK.getStatusCode()) - .body("data.unreadCount", equalTo(1)); - - long createAccountId = JsonPath.from(getNotifications.getBody().asString()).getLong("data.notifications[0].id"); + .body("data.unreadCount", equalTo(2)); - Response markReadNoPerms = UtilIT.markNotificationAsRead(createAccountId, nopermsApiToken); - markReadNoPerms.prettyPrint(); + Response markReadNoPerms = UtilIT.markNotificationAsRead(createAccountId, noPermsApiToken); markReadNoPerms.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); Response markRead = UtilIT.markNotificationAsRead(createAccountId, authorApiToken); - markRead.prettyPrint(); markRead.then().assertThat().statusCode(OK.getStatusCode()); Response getNotifications2 = UtilIT.getNotifications(authorApiToken); - getNotifications2.prettyPrint(); getNotifications2.then().assertThat() - .body("data.notifications[0].type", equalTo("CREATEACC")) - .body("data.notifications[0].displayAsRead", equalTo(true)) - .body("data.notifications[1]", equalTo(null)) + .body("data.notifications.size()", equalTo(2)) .statusCode(OK.getStatusCode()); - Response deleteNotificationNoPerms = UtilIT.deleteNotification(createAccountId, nopermsApiToken); - deleteNotificationNoPerms.prettyPrint(); + firstNotificationType = JsonPath.from(getNotifications2.body().asString()).getString("data.notifications[0].type"); + secondNotificationType = JsonPath.from(getNotifications2.body().asString()).getString("data.notifications[1].type"); + if (firstNotificationType.equals(CREATEDV.toString())) { + assertEquals(CREATEACC.toString(), secondNotificationType); + assertTrue(JsonPath.from(getNotifications2.body().asString()).getBoolean("data.notifications[1].displayAsRead")); + assertFalse(JsonPath.from(getNotifications2.body().asString()).getBoolean("data.notifications[0].displayAsRead")); + } else if (firstNotificationType.equals(CREATEACC.toString())) { + assertEquals(CREATEDV.toString(), secondNotificationType); + assertTrue(JsonPath.from(getNotifications2.body().asString()).getBoolean("data.notifications[0].displayAsRead")); + assertFalse(JsonPath.from(getNotifications2.body().asString()).getBoolean("data.notifications[1].displayAsRead")); + } else { + fail("Unexpected notification type: " + firstNotificationType); + } + + Response deleteNotificationNoPerms = UtilIT.deleteNotification(createAccountId, noPermsApiToken); deleteNotificationNoPerms.then().assertThat().statusCode(NOT_FOUND.getStatusCode()); Response deleteNotification = UtilIT.deleteNotification(createAccountId, authorApiToken); - deleteNotification.prettyPrint(); deleteNotification.then().assertThat().statusCode(OK.getStatusCode()); Response getNotifications3 = UtilIT.getNotifications(authorApiToken); - getNotifications3.prettyPrint(); getNotifications3.then().assertThat() - .body("data.notifications[0]", equalTo(null)) + .body("data.notifications[0].type", equalTo(CREATEDV.toString())) + .body("data.notifications.size()", equalTo(1)) + .statusCode(OK.getStatusCode()); + + // SendNotificationOnDatasetCreation setting is true + + createAuthor = UtilIT.createRandomUser(); + createAuthor.then().assertThat() + .statusCode(OK.getStatusCode()); + authorApiToken = UtilIT.getApiTokenFromResponse(createAuthor); + + Response enableSendNotificationOnDatasetCreationSettingResponse = UtilIT.enableSetting(SettingsServiceBean.Key.SendNotificationOnDatasetCreation); + enableSendNotificationOnDatasetCreationSettingResponse.then().assertThat() + .statusCode(OK.getStatusCode()); + + createDataverseResponse = UtilIT.createRandomDataverse(authorApiToken); + createDataverseResponse.then().assertThat() + .statusCode(CREATED.getStatusCode()); + dataverseAlias = UtilIT.getAliasFromResponse(createDataverseResponse); + + createDataset = UtilIT.createRandomDatasetViaNativeApi(dataverseAlias, authorApiToken); + createDataset.then().assertThat() + .statusCode(CREATED.getStatusCode()); + + getNotifications = UtilIT.getNotifications(authorApiToken); + getNotifications.then().assertThat() + .body("data.notifications[0].displayAsRead", equalTo(false)) + .body("data.notifications[1].displayAsRead", equalTo(false)) + .body("data.notifications[2].displayAsRead", equalTo(false)) + .body("data.notifications.size()", equalTo(3)) + .statusCode(OK.getStatusCode()); + + List notificationTypes = JsonPath.from(getNotifications.body().asString()).getList("data.notifications.type"); + + List expectedTypes = Arrays.asList(CREATEACC.toString(), CREATEDV.toString(), DATASETCREATED.toString()); + + assertTrue(notificationTypes.containsAll(expectedTypes) && expectedTypes.containsAll(notificationTypes)); + + disableSendNotificationOnDatasetCreationSetting(); + + // inAppNotificationFormat optional query parameter test cases + + // inAppNotificationFormat = false (default) + + createAuthor = UtilIT.createRandomUser(); + createAuthor.then().assertThat() + .statusCode(OK.getStatusCode()); + authorApiToken = UtilIT.getApiTokenFromResponse(createAuthor); + + getNotifications = UtilIT.getNotifications(authorApiToken); + getNotifications.then().assertThat() + .body("data.notifications[0].displayAsRead", equalTo(false)) + .body("data.notifications.size()", equalTo(1)) + // In-App fields should be null + .body("data.notifications[0].installationBrandName", equalTo(null)) + .body("data.notifications[0].userGuidesBaseUrl", equalTo(null)) + .body("data.notifications[0].userGuidesVersion", equalTo(null)) + .body("data.notifications[0].userGuidesSectionPath", equalTo(null)) + // Email-related fields should be present + .body("data.notifications[0].subjectText", equalTo("Root: Your account has been created")) + .body("data.notifications[0].messageText", containsString("Hello,")) + .statusCode(OK.getStatusCode()); + + // inAppNotificationFormat = true + + createAuthor = UtilIT.createRandomUser(); + createAuthor.then().assertThat() .statusCode(OK.getStatusCode()); + authorApiToken = UtilIT.getApiTokenFromResponse(createAuthor); + getNotifications = UtilIT.getNotifications(authorApiToken, true); + getNotifications.then().assertThat() + .body("data.notifications[0].displayAsRead", equalTo(false)) + .body("data.notifications.size()", equalTo(1)) + // In-App fields should be present + .body("data.notifications[0].installationBrandName", equalTo("Root")) + .body("data.notifications[0].userGuidesBaseUrl", equalTo("https://guides.dataverse.org")) + .body("data.notifications[0].userGuidesSectionPath", equalTo("user/index.html")) + .body("data.notifications[0].userGuidesVersion", not(equalTo(null))) + // Email-related fields should be null + .body("data.notifications[0].subjectText", equalTo(null)) + .body("data.notifications[0].messageText", equalTo(null)) + .statusCode(OK.getStatusCode()); + } + + private static void disableSendNotificationOnDatasetCreationSetting() { + Response disableSendNotificationOnDatasetCreationSettingResponse = UtilIT.deleteSetting(SettingsServiceBean.Key.SendNotificationOnDatasetCreation); + disableSendNotificationOnDatasetCreationSettingResponse.then().assertThat() + .statusCode(OK.getStatusCode()); } } diff --git a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java index 24f2adbb3ed..2f7b3d94990 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/UtilIT.java @@ -1702,12 +1702,16 @@ static Response returnDatasetToAuthor(String datasetPersistentId, JsonObject jso } static Response getNotifications(String apiToken) { + return getNotifications(apiToken, false); + } + + static Response getNotifications(String apiToken, boolean inAppNotificationFormat) { RequestSpecification requestSpecification = given(); if (apiToken != null) { requestSpecification = given() .header(UtilIT.API_TOKEN_HTTP_HEADER, apiToken); } - return requestSpecification.get("/api/notifications/all"); + return requestSpecification.get("/api/notifications/all?inAppNotificationFormat=" + inAppNotificationFormat); } static Response getUnreadNotificationsCount(String apiToken) { diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinterTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinterTest.java new file mode 100644 index 00000000000..f0b2b9dc3f9 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/InAppNotificationsJsonPrinterTest.java @@ -0,0 +1,503 @@ +package edu.harvard.iq.dataverse.util.json; + +import edu.harvard.iq.dataverse.*; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.branding.BrandingUtil; +import edu.harvard.iq.dataverse.pidproviders.doi.AbstractDOIProvider; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.json.JsonArrayBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; + +import static edu.harvard.iq.dataverse.util.json.InAppNotificationsJsonPrinter.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class InAppNotificationsJsonPrinterTest { + + @Mock + private DataverseServiceBean dataverseService; + @Mock + private DatasetServiceBean datasetService; + @Mock + private DatasetVersionServiceBean datasetVersionService; + @Mock + private DataFileServiceBean dataFileService; + @Mock + private PermissionServiceBean permissionService; + @Mock + private SystemConfig systemConfig; + + @InjectMocks + private InAppNotificationsJsonPrinter sut; + + @Mock + private NullSafeJsonBuilder notificationJson; + @Mock + private AuthenticatedUser authenticatedUser; + @Mock + private AuthenticatedUser requestor; + + private UserNotification userNotification; + + private final GlobalId testGlobalId = new GlobalId(AbstractDOIProvider.DOI_PROTOCOL, "10.5072", "FK2/BYM3IW", "/", AbstractDOIProvider.DOI_RESOLVER_URL, null); + + @BeforeEach + public void setUp() { + userNotification = new UserNotification(); + } + + @Test + public void testAddFieldsByType_assignRole_dataverse() { + userNotification.setType(UserNotification.Type.ASSIGNROLE); + userNotification.setObjectId(1L); + + Dataverse dataverse = mock(Dataverse.class); + when(dataverse.getAlias()).thenReturn("testdv"); + when(dataverse.getDisplayName()).thenReturn("Test Dataverse"); + when(dataverseService.find(1L)).thenReturn(dataverse); + when(permissionService.getEffectiveRoleAssignments(authenticatedUser, dataverse)).thenReturn(Collections.emptyList()); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(eq(KEY_ROLE_ASSIGNMENTS), any(JsonArrayBuilder.class)); + verify(notificationJson).add(KEY_DATAVERSE_ALIAS, "testdv"); + verify(notificationJson).add(KEY_DATAVERSE_DISPLAY_NAME, "Test Dataverse"); + verifyNoInteractions(datasetService, dataFileService); + } + + @Test + public void testAddFieldsByType_assignRole_dataset() { + userNotification.setType(UserNotification.Type.ASSIGNROLE); + userNotification.setObjectId(1L); + + Dataset dataset = mock(Dataset.class); + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("Test Dataset"); + + when(dataverseService.find(1L)).thenReturn(null); + when(datasetService.find(1L)).thenReturn(dataset); + when(permissionService.getEffectiveRoleAssignments(authenticatedUser, dataset)).thenReturn(Collections.emptyList()); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(eq(KEY_ROLE_ASSIGNMENTS), any(JsonArrayBuilder.class)); + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "Test Dataset"); + verifyNoInteractions(dataFileService); + } + + @Test + public void testAddFieldsByType_revokeRole_dataFile() { + userNotification.setType(UserNotification.Type.REVOKEROLE); + userNotification.setObjectId(1L); + + DataFile dataFile = mock(DataFile.class); + Dataset owner = mock(Dataset.class); + + when(dataFile.getOwner()).thenReturn(owner); + when(owner.getGlobalId()).thenReturn(testGlobalId); + when(owner.getDisplayName()).thenReturn("Owner Dataset"); + + when(dataverseService.find(1L)).thenReturn(null); + when(datasetService.find(1L)).thenReturn(null); + when(dataFileService.find(1L)).thenReturn(dataFile); + when(permissionService.getEffectiveRoleAssignments(authenticatedUser, dataFile)).thenReturn(Collections.emptyList()); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(eq(KEY_ROLE_ASSIGNMENTS), any(JsonArrayBuilder.class)); + verify(notificationJson).add(KEY_OWNER_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Owner Dataset"); + } + + @Test + public void testAddFieldsByType_revokeRole_objectDeleted() { + userNotification.setType(UserNotification.Type.REVOKEROLE); + userNotification.setObjectId(1L); + + when(dataverseService.find(1L)).thenReturn(null); + when(datasetService.find(1L)).thenReturn(null); + when(dataFileService.find(1L)).thenReturn(null); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_OBJECT_DELETED, true); + } + + @Test + public void testAddFieldsByType_createDv_dvHasOwner() { + userNotification.setType(UserNotification.Type.CREATEDV); + userNotification.setObjectId(1L); + + Dataverse dataverse = mock(Dataverse.class); + Dataverse owner = mock(Dataverse.class); + + when(dataverse.getAlias()).thenReturn("childDv"); + when(dataverse.getDisplayName()).thenReturn("Child Dataverse"); + when(dataverse.getOwner()).thenReturn(owner); + + when(owner.getAlias()).thenReturn("parentDv"); + when(owner.getDisplayName()).thenReturn("Parent Dataverse"); + + when(dataverseService.find(1L)).thenReturn(dataverse); + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATAVERSE_ALIAS, "childDv"); + verify(notificationJson).add(KEY_DATAVERSE_DISPLAY_NAME, "Child Dataverse"); + verify(notificationJson).add(KEY_OWNER_ALIAS, "parentDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Parent Dataverse"); + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_DATAVERSE_MANAGEMENT_HTML); + } + + @Test + public void testAddFieldsByType_createDv_dvHasNoOwner() { + userNotification.setType(UserNotification.Type.CREATEDV); + userNotification.setObjectId(1L); + + Dataverse dataverse = mock(Dataverse.class); + + when(dataverse.getAlias()).thenReturn("dv"); + when(dataverse.getDisplayName()).thenReturn("Dataverse"); + when(dataverse.getOwner()).thenReturn(null); + + when(dataverseService.find(1L)).thenReturn(dataverse); + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATAVERSE_ALIAS, "dv"); + verify(notificationJson).add(KEY_DATAVERSE_DISPLAY_NAME, "Dataverse"); + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_DATAVERSE_MANAGEMENT_HTML); + } + + @Test + public void testAddFieldsByType_createDv_objectDeleted() { + userNotification.setType(UserNotification.Type.CREATEDV); + userNotification.setObjectId(1L); + + when(dataverseService.find(1L)).thenReturn(null); + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_DATAVERSE_MANAGEMENT_HTML); + verify(notificationJson).add(KEY_OBJECT_DELETED, true); + } + + @Test + public void testAddFieldsByType_requestFileAccess() { + userNotification.setType(UserNotification.Type.REQUESTFILEACCESS); + userNotification.setObjectId(1L); + userNotification.setRequestor(requestor); + + when(requestor.getFirstName()).thenReturn("John"); + when(requestor.getLastName()).thenReturn("Doe"); + when(requestor.getEmail()).thenReturn("johndoe@example.com"); + + DataFile dataFile = mock(DataFile.class); + when(dataFile.getId()).thenReturn(1L); + when(dataFile.getDisplayName()).thenReturn("Test File"); + when(dataFileService.find(1L)).thenReturn(dataFile); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_REQUESTOR_FIRST_NAME, "John"); + verify(notificationJson).add(KEY_REQUESTOR_LAST_NAME, "Doe"); + verify(notificationJson).add(KEY_REQUESTOR_EMAIL, "johndoe@example.com"); + verify(notificationJson).add(KEY_DATAFILE_ID, Long.valueOf("1")); + verify(notificationJson).add(KEY_DATAFILE_DISPLAY_NAME, "Test File"); + } + + @Test + public void testAddFieldsByType_grantFileAccess() { + userNotification.setType(UserNotification.Type.GRANTFILEACCESS); + userNotification.setObjectId(1L); + + DataFile dataFile = mock(DataFile.class); + when(dataFile.getId()).thenReturn(1L); + when(dataFile.getDisplayName()).thenReturn("Granted File"); + when(dataFileService.find(1L)).thenReturn(dataFile); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATAFILE_ID, Long.valueOf("1")); + verify(notificationJson).add(KEY_DATAFILE_DISPLAY_NAME, "Granted File"); + verify(notificationJson, never()).add(eq(KEY_REQUESTOR_FIRST_NAME), anyString()); + } + + @Test + public void testAddFieldsByType_grantFileAccess_objectDeleted() { + userNotification.setType(UserNotification.Type.GRANTFILEACCESS); + userNotification.setObjectId(1L); + + when(dataFileService.find(1L)).thenReturn(null); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_OBJECT_DELETED, true); + } + + @Test + public void testAddFieldsByType_createDs() { + userNotification.setType(UserNotification.Type.CREATEDS); + userNotification.setObjectId(1L); + + DatasetVersion datasetVersion = mock(DatasetVersion.class); + Dataset dataset = mock(Dataset.class); + Dataverse owner = mock(Dataverse.class); + + when(owner.getAlias()).thenReturn("ownerDv"); + when(owner.getDisplayName()).thenReturn("Owner Dataverse"); + + when(datasetVersion.getDataset()).thenReturn(dataset); + + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("My Dataset"); + when(dataset.getOwner()).thenReturn(owner); + + when(datasetVersionService.find(1L)).thenReturn(datasetVersion); + + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_DATASET_MANAGEMENT_HTML); + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "My Dataset"); + verify(notificationJson).add(KEY_OWNER_ALIAS, "ownerDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Owner Dataverse"); + } + + @Test + public void testAddFieldsByType_submittedDs() { + userNotification.setType(UserNotification.Type.SUBMITTEDDS); + userNotification.setObjectId(1L); + userNotification.setRequestor(requestor); + + Dataset dataset = mock(Dataset.class); + Dataverse owner = mock(Dataverse.class); + + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("Submitted Dataset"); + when(dataset.getOwner()).thenReturn(owner); + when(datasetService.find(1L)).thenReturn(dataset); + + when(owner.getAlias()).thenReturn("reviewDv"); + when(owner.getDisplayName()).thenReturn("Review Dataverse"); + + when(requestor.getFirstName()).thenReturn("Jane"); + when(requestor.getLastName()).thenReturn("Submitter"); + when(requestor.getEmail()).thenReturn("j.submitter@example.com"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "Submitted Dataset"); + verify(notificationJson).add(KEY_OWNER_ALIAS, "reviewDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Review Dataverse"); + verify(notificationJson).add(KEY_REQUESTOR_FIRST_NAME, "Jane"); + verify(notificationJson).add(KEY_REQUESTOR_LAST_NAME, "Submitter"); + verify(notificationJson).add(KEY_REQUESTOR_EMAIL, "j.submitter@example.com"); + } + + @Test + public void testAddFieldsByType_publishedDs() { + userNotification.setType(UserNotification.Type.PUBLISHEDDS); + userNotification.setObjectId(1L); + + DatasetVersion datasetVersion = mock(DatasetVersion.class); + Dataset dataset = mock(Dataset.class); + Dataverse owner = mock(Dataverse.class); + + when(owner.getAlias()).thenReturn("ownerDv"); + when(owner.getDisplayName()).thenReturn("Owner Dataverse"); + + when(datasetVersion.getDataset()).thenReturn(dataset); + + when(dataset.getOwner()).thenReturn(owner); + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("Published Dataset"); + + when(datasetVersionService.find(1L)).thenReturn(datasetVersion); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "Published Dataset"); + verify(notificationJson, never()).add(eq(KEY_CURATION_STATUS), anyString()); + verify(notificationJson).add(KEY_OWNER_ALIAS, "ownerDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Owner Dataverse"); + } + + @Test + public void testAddFieldsByType_statusUpdated() { + userNotification.setType(UserNotification.Type.STATUSUPDATED); + userNotification.setObjectId(1L); + + DatasetVersion datasetVersion = mock(DatasetVersion.class); + Dataset dataset = mock(Dataset.class); + Dataverse owner = mock(Dataverse.class); + + when(owner.getAlias()).thenReturn("ownerDv"); + when(owner.getDisplayName()).thenReturn("Owner Dataverse"); + + CurationStatus curationStatusMock = mock(CurationStatus.class); + when(curationStatusMock.getLabel()).thenReturn("testStatus"); + + when(datasetVersion.getDataset()).thenReturn(dataset); + when(datasetVersion.getCurrentCurationStatus()).thenReturn(curationStatusMock); + when(datasetVersionService.find(1L)).thenReturn(datasetVersion); + + when(dataset.getOwner()).thenReturn(owner); + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("Status Update Dataset"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "Status Update Dataset"); + verify(notificationJson).add(eq(KEY_CURATION_STATUS), any(String.class)); + verify(notificationJson).add(KEY_OWNER_ALIAS, "ownerDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Owner Dataverse"); + } + + @Test + public void testAddFieldsByType_statusUpdated_objectDeleted() { + userNotification.setType(UserNotification.Type.STATUSUPDATED); + userNotification.setObjectId(1L); + + when(datasetVersionService.find(1L)).thenReturn(null); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_OBJECT_DELETED, true); + } + + @Test + public void testAddFieldsByType_createAcc() { + userNotification.setType(UserNotification.Type.CREATEACC); + try (MockedStatic mockedBrandingUtil = mockStatic(BrandingUtil.class)) { + mockedBrandingUtil.when(BrandingUtil::getInstallationBrandName).thenReturn("My Test Brand Name"); + + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_INSTALLATION_BRAND_NAME, "My Test Brand Name"); + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_USER_HTML); + } + } + + @Test + public void testAddFieldsByType_ingestCompleted() { + userNotification.setType(UserNotification.Type.INGESTCOMPLETED); + userNotification.setObjectId(1L); + + Dataset dataset = mock(Dataset.class); + Dataverse owner = mock(Dataverse.class); + + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("Ingested Dataset"); + when(dataset.getOwner()).thenReturn(owner); + + when(owner.getAlias()).thenReturn("ownerDv"); + when(owner.getDisplayName()).thenReturn("Owner Dataverse"); + + when(datasetService.find(1L)).thenReturn(dataset); + + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "Ingested Dataset"); + verify(notificationJson).add(KEY_OWNER_ALIAS, "ownerDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Owner Dataverse"); + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_DATASET_MANAGEMENT_TABULAR_FILES_HTML); + } + + @Test + public void testAddFieldsByType_ingestCompleted_objectDeleted() { + userNotification.setType(UserNotification.Type.INGESTCOMPLETED); + userNotification.setObjectId(1L); + + when(datasetService.find(1L)).thenReturn(null); + + when(systemConfig.getGuidesBaseUrl(false)).thenReturn("http://guides.dataverse.org"); + when(systemConfig.getGuidesVersion()).thenReturn("1.0"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_OBJECT_DELETED, true); + verify(notificationJson).add(KEY_GUIDES_BASE_URL, "http://guides.dataverse.org"); + verify(notificationJson).add(KEY_GUIDES_VERSION, "1.0"); + verify(notificationJson).add(KEY_GUIDES_SECTION_PATH, GUIDES_SECTION_PATH_DATASET_MANAGEMENT_TABULAR_FILES_HTML); + } + + @Test + public void testAddFieldsByType_datasetMentioned() { + userNotification.setType(UserNotification.Type.DATASETMENTIONED); + userNotification.setObjectId(1L); + userNotification.setAdditionalInfo("Mentioned in another dataset."); + + Dataset dataset = mock(Dataset.class); + Dataverse owner = mock(Dataverse.class); + + when(dataset.getGlobalId()).thenReturn(testGlobalId); + when(dataset.getDisplayName()).thenReturn("Mentioned Dataset"); + when(dataset.getOwner()).thenReturn(owner); + when(datasetService.find(1L)).thenReturn(dataset); + + when(owner.getAlias()).thenReturn("ownerDv"); + when(owner.getDisplayName()).thenReturn("Owner Dataverse"); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verify(notificationJson).add(KEY_DATASET_PERSISTENT_ID, testGlobalId.toString()); + verify(notificationJson).add(KEY_DATASET_DISPLAY_NAME, "Mentioned Dataset"); + verify(notificationJson).add(KEY_OWNER_ALIAS, "ownerDv"); + verify(notificationJson).add(KEY_OWNER_DISPLAY_NAME, "Owner Dataverse"); + verify(notificationJson).add(KEY_ADDITIONAL_INFO, "Mentioned in another dataset."); + } + + @Test + public void testAddFieldsByType_noOpType() { + // APIGENERATED is a valid type but is not handled by the switch statement, + // so no fields should be added. + userNotification.setType(UserNotification.Type.APIGENERATED); + + sut.addFieldsByType(notificationJson, authenticatedUser, userNotification); + + verifyNoInteractions(notificationJson); + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java index edd0b1b05e1..36ff11fe4bb 100644 --- a/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/util/json/JsonPrinterTest.java @@ -210,11 +210,7 @@ public void testDatasetContactOutOfBoxNoPrivacy() { datasetContactField.setDatasetFieldCompoundValues(vals); fields.add(datasetContactField); - SettingsServiceBean nullServiceBean = null; - DatasetFieldServiceBean nullDFServiceBean = null; - DataverseFieldTypeInputLevelServiceBean nullDFILServiceBean = null; - DatasetServiceBean nullDatasetServiceBean = null; - JsonPrinter.injectSettingsService(nullServiceBean, nullDFServiceBean, nullDFILServiceBean, nullDatasetServiceBean); + JsonPrinter.injectSettingsService(null, null, null, null, null, null); JsonObject jsonObject = JsonPrinter.json(block, fields).build(); assertNotNull(jsonObject); @@ -255,10 +251,7 @@ public void testDatasetContactWithPrivacy() { datasetContactField.setDatasetFieldCompoundValues(vals); fields.add(datasetContactField); - DatasetFieldServiceBean nullDFServiceBean = null; - DataverseFieldTypeInputLevelServiceBean nullDFILServiceBean = null; - DatasetServiceBean nullDatasetServiceBean = null; - JsonPrinter.injectSettingsService(new MockSettingsSvc(), nullDFServiceBean, nullDFILServiceBean, nullDatasetServiceBean); + JsonPrinter.injectSettingsService(new MockSettingsSvc(), null, null, null, null, null); JsonObject jsonObject = JsonPrinter.json(block, fields).build(); assertNotNull(jsonObject); @@ -308,10 +301,7 @@ public void testDatasetFieldTypesWithChildren() { block.setDatasetFieldTypes(datasetFieldTypes); - DatasetFieldServiceBean nullDFServiceBean = null; - DataverseFieldTypeInputLevelServiceBean nullDFILServiceBean = null; - DatasetServiceBean nullDatasetServiceBean = null; - JsonPrinter.injectSettingsService(new MockSettingsSvc(), nullDFServiceBean, nullDFILServiceBean, nullDatasetServiceBean); + JsonPrinter.injectSettingsService(new MockSettingsSvc(), null, null ,null, null, null); JsonObject jsonObject = JsonPrinter.json(block).build(); assertNotNull(jsonObject);