diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/HttpApiV1Module.java b/src/main/java/io/cryostat/net/web/http/api/v1/HttpApiV1Module.java index 8608b113ec..6488671d12 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/HttpApiV1Module.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/HttpApiV1Module.java @@ -44,6 +44,7 @@ import io.cryostat.MainModule; import io.cryostat.core.sys.Clock; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.platform.PlatformClient; @@ -102,9 +103,15 @@ static TargetRecordingPatchSave provideTargetRecordingPatchSave( @Named(MainModule.RECORDINGS_PATH) Path recordingsPath, TargetConnectionManager targetConnectionManager, Clock clock, - PlatformClient platformClient) { + PlatformClient platformClient, + NotificationFactory notificationFactory) { return new TargetRecordingPatchSave( - fs, recordingsPath, targetConnectionManager, clock, platformClient); + fs, + recordingsPath, + targetConnectionManager, + clock, + platformClient, + notificationFactory); } @Provides diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java index b8c99bb0c5..72e4d8b7e7 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java @@ -39,15 +39,18 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Map; import javax.inject.Inject; import javax.inject.Named; import io.cryostat.MainModule; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; +import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; import io.vertx.core.http.HttpMethod; @@ -59,17 +62,21 @@ class RecordingDeleteHandler extends AbstractAuthenticatedRequestHandler { private final ReportService reportService; private final FileSystem fs; private final Path savedRecordingsPath; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "RecordingDeleted"; @Inject RecordingDeleteHandler( AuthManager auth, ReportService reportService, FileSystem fs, + NotificationFactory notificationFactory, @Named(MainModule.RECORDINGS_PATH) Path savedRecordingsPath) { super(auth); this.reportService = reportService; this.fs = fs; this.savedRecordingsPath = savedRecordingsPath; + this.notificationFactory = notificationFactory; } @Override @@ -106,6 +113,13 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception { throw new HttpStatusException(404, recordingName); } fs.deleteIfExists(path); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("recording", recordingName)) + .build() + .send(); } catch (IOException e) { throw new HttpStatusException(500, e.getMessage(), e); } finally { diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java index 1d6359ceec..16be53f11f 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java @@ -53,6 +53,7 @@ import io.cryostat.MainModule; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.HttpServer; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; @@ -81,6 +82,8 @@ class RecordingsPostHandler extends AbstractAuthenticatedRequestHandler { private final Path savedRecordingsPath; private final Gson gson; private final Logger logger; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "RecordingSaved"; @Inject RecordingsPostHandler( @@ -89,13 +92,15 @@ class RecordingsPostHandler extends AbstractAuthenticatedRequestHandler { FileSystem fs, @Named(MainModule.RECORDINGS_PATH) Path savedRecordingsPath, Gson gson, - Logger logger) { + Logger logger, + NotificationFactory notificationFactory) { super(auth); this.vertx = httpServer.getVertx(); this.fs = fs; this.savedRecordingsPath = savedRecordingsPath; this.gson = gson; this.logger = logger; + this.notificationFactory = notificationFactory; } @Override @@ -191,6 +196,14 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception { .end(gson.toJson(Map.of("name", res2.result()))); logger.info("Recording saved as {}", res2.result()); + + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("recording", res2.result())) + .build() + .send(); })); } diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java index c6a6709c1a..85178bd9fc 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java @@ -37,17 +37,20 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.Map; import java.util.Optional; import javax.inject.Inject; import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.reports.ReportService; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; +import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; import io.vertx.core.http.HttpMethod; @@ -58,13 +61,17 @@ class TargetRecordingDeleteHandler extends AbstractAuthenticatedRequestHandler { private final TargetConnectionManager targetConnectionManager; private final ReportService reportService; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "RecordingDeleted"; @Inject TargetRecordingDeleteHandler( AuthManager auth, TargetConnectionManager targetConnectionManager, + NotificationFactory notificationFactory, ReportService reportService) { super(auth); + this.notificationFactory = notificationFactory; this.targetConnectionManager = targetConnectionManager; this.reportService = reportService; } @@ -103,6 +110,18 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception { if (descriptor.isPresent()) { connection.getService().close(descriptor.get()); reportService.delete(connectionDescriptor, recordingName); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message( + Map.of( + "recording", + recordingName, + "target", + connectionDescriptor.getTargetId())) + .build() + .send(); ctx.response().setStatusCode(200); ctx.response().end(); } else { diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSave.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSave.java index 7a904a0517..aefb82bd5b 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSave.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSave.java @@ -41,6 +41,7 @@ import java.io.InputStream; import java.nio.file.Path; import java.time.temporal.ChronoUnit; +import java.util.Map; import java.util.Optional; import javax.inject.Inject; @@ -52,8 +53,10 @@ import io.cryostat.core.net.JFRConnection; import io.cryostat.core.sys.Clock; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.platform.PlatformClient; import io.vertx.ext.web.RoutingContext; @@ -66,6 +69,8 @@ class TargetRecordingPatchSave { private final TargetConnectionManager targetConnectionManager; private final Clock clock; private final PlatformClient platformClient; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "RecordingArchived"; @Inject TargetRecordingPatchSave( @@ -73,12 +78,14 @@ class TargetRecordingPatchSave { @Named(MainModule.RECORDINGS_PATH) Path recordingsPath, TargetConnectionManager targetConnectionManager, Clock clock, - PlatformClient platformClient) { + PlatformClient platformClient, + NotificationFactory notificationFactory) { this.fs = fs; this.recordingsPath = recordingsPath; this.targetConnectionManager = targetConnectionManager; this.clock = clock; this.platformClient = platformClient; + this.notificationFactory = notificationFactory; } void handle(RoutingContext ctx, ConnectionDescriptor connectionDescriptor) throws Exception { @@ -108,6 +115,14 @@ void handle(RoutingContext ctx, ConnectionDescriptor connectionDescriptor) throw }); ctx.response().setStatusCode(200); ctx.response().end(saveName); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message( + Map.of("recording", saveName, "target", connectionDescriptor.getTargetId())) + .build() + .send(); } private String saveRecording(JFRConnection connection, IRecordingDescriptor descriptor) diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java index 998abad9db..af85bc1671 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java @@ -39,6 +39,7 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; @@ -60,6 +61,7 @@ import io.cryostat.core.templates.Template; import io.cryostat.core.templates.TemplateType; import io.cryostat.jmc.serialization.HyperlinkedSerializableRecordingDescriptor; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.web.WebServer; @@ -96,6 +98,8 @@ class TargetRecordingsPostHandler extends AbstractAuthenticatedRequestHandler { private final EventOptionsBuilder.Factory eventOptionsBuilderFactory; private final Provider webServerProvider; private final Gson gson; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "RecordingCreated"; @Inject TargetRecordingsPostHandler( @@ -104,13 +108,15 @@ class TargetRecordingsPostHandler extends AbstractAuthenticatedRequestHandler { RecordingOptionsBuilderFactory recordingOptionsBuilderFactory, EventOptionsBuilder.Factory eventOptionsBuilderFactory, Provider webServerProvider, - Gson gson) { + Gson gson, + NotificationFactory notificationFactory) { super(auth); this.targetConnectionManager = targetConnectionManager; this.recordingOptionsBuilderFactory = recordingOptionsBuilderFactory; this.eventOptionsBuilderFactory = eventOptionsBuilderFactory; this.webServerProvider = webServerProvider; this.gson = gson; + this.notificationFactory = notificationFactory; } @Override @@ -187,7 +193,19 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception { .start( recordingOptions, enableEvents(connection, eventSpecifier)); - + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message( + Map.of( + "recording", + recordingName, + "target", + getConnectionDescriptorFromContext(ctx) + .getTargetId())) + .build() + .send(); return getDescriptorByName(connection, recordingName) .map( d -> { diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java index b519617e4f..f5b995133a 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java @@ -37,12 +37,16 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.Map; + import javax.inject.Inject; import io.cryostat.core.templates.LocalStorageTemplateService; import io.cryostat.core.templates.MutableTemplateService.InvalidEventTemplateException; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; +import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; import io.vertx.core.http.HttpMethod; @@ -52,11 +56,17 @@ class TemplateDeleteHandler extends AbstractAuthenticatedRequestHandler { private final LocalStorageTemplateService templateService; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "TemplateDeleted"; @Inject - TemplateDeleteHandler(AuthManager auth, LocalStorageTemplateService templateService) { + TemplateDeleteHandler( + AuthManager auth, + LocalStorageTemplateService templateService, + NotificationFactory notificationFactory) { super(auth); this.templateService = templateService; + this.notificationFactory = notificationFactory; } @Override @@ -85,6 +95,13 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception { try { this.templateService.deleteTemplate(templateName); ctx.response().end(); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("template", templateName)) + .build() + .send(); } catch (InvalidEventTemplateException iete) { throw new HttpStatusException(400, iete.getMessage(), iete); } diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java index c10c903b18..91e1291570 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java @@ -39,6 +39,7 @@ import java.io.InputStream; import java.nio.file.Path; +import java.util.Map; import javax.inject.Inject; @@ -47,8 +48,10 @@ import io.cryostat.core.templates.LocalStorageTemplateService; import io.cryostat.core.templates.MutableTemplateService.InvalidEventTemplateException; import io.cryostat.core.templates.MutableTemplateService.InvalidXmlException; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; +import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; import io.vertx.core.http.HttpMethod; @@ -63,14 +66,18 @@ class TemplatesPostHandler extends AbstractAuthenticatedRequestHandler { private final LocalStorageTemplateService templateService; private final FileSystem fs; private final Logger logger; + private final NotificationFactory notificationFactory; + private static final String NOTIFICATION_CATEGORY = "TemplateUploaded"; @Inject TemplatesPostHandler( AuthManager auth, LocalStorageTemplateService templateService, FileSystem fs, + NotificationFactory notificationFactory, Logger logger) { super(auth); + this.notificationFactory = notificationFactory; this.templateService = templateService; this.fs = fs; this.logger = logger; @@ -106,6 +113,13 @@ public void handleAuthenticated(RoutingContext ctx) throws Exception { logger.info("Received unexpected file upload named {}", u.name()); continue; } + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("template", u.uploadedFileName())) + .build() + .send(); templateService.addTemplate(is); } finally { fs.deleteIfExists(path); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java index 9fa7a69e08..82cf7bbb04 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java @@ -37,14 +37,20 @@ */ package io.cryostat.net.web.http.api.v1; +import static org.mockito.Mockito.lenient; + import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.web.http.HttpMimeType; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; @@ -68,13 +74,30 @@ class RecordingDeleteHandlerTest { @Mock ReportService reportService; @Mock FileSystem fs; @Mock Path savedRecordingsPath; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; @Mock RoutingContext ctx; @Mock HttpServerResponse resp; @BeforeEach void setup() { - this.handler = new RecordingDeleteHandler(auth, reportService, fs, savedRecordingsPath); + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new RecordingDeleteHandler( + auth, reportService, fs, notificationFactory, savedRecordingsPath); } @Test @@ -121,6 +144,13 @@ void shouldDeleteIfRecordingFound() throws Exception { Mockito.verify(reportService).delete(recordingName); Mockito.verify(resp).setStatusCode(200); Mockito.verify(resp).end(); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("RecordingDeleted"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder).message(Map.of("recording", recordingName)); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); } @Test diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java index ca9a5be1ab..a583fe0991 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java @@ -38,18 +38,20 @@ package io.cryostat.net.web.http.api.v1; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; import java.nio.file.Path; import java.util.HashSet; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.HttpServer; import io.cryostat.net.web.http.HttpMimeType; @@ -83,9 +85,24 @@ class RecordingsPostHandlerTest { @Mock FileSystem cryoFs; @Mock Path recordingsPath; @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; @BeforeEach void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); when(httpServer.getVertx()).thenReturn(vertx); this.handler = new RecordingsPostHandler( @@ -94,7 +111,8 @@ void setup() { cryoFs, recordingsPath, MainModule.provideGson(logger), - logger); + logger, + notificationFactory); } @Test @@ -233,5 +251,12 @@ public boolean failed() { InOrder inOrder = Mockito.inOrder(rep); inOrder.verify(rep).putHeader(HttpHeaders.CONTENT_TYPE, HttpMimeType.JSON.mime()); inOrder.verify(rep).end("{\"name\":\"" + filename + "\"}"); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("RecordingSaved"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder).message(Map.of("recording", filename)); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); } } diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java index e28ec861f6..e4587997de 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java @@ -37,7 +37,10 @@ */ package io.cryostat.net.web.http.api.v1; +import static org.mockito.Mockito.lenient; + import java.util.List; +import java.util.Map; import org.openjdk.jmc.common.unit.IQuantity; import org.openjdk.jmc.common.unit.QuantityConversionException; @@ -45,10 +48,13 @@ import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; import io.cryostat.core.net.JFRConnection; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.web.http.HttpMimeType; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; @@ -75,6 +81,9 @@ class TargetRecordingDeleteHandlerTest { TargetRecordingDeleteHandler handler; @Mock AuthManager auth; @Mock TargetConnectionManager targetConnectionManager; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; @Mock ReportService reportService; @Mock RoutingContext ctx; @@ -85,8 +94,21 @@ class TargetRecordingDeleteHandlerTest { @BeforeEach void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); this.handler = - new TargetRecordingDeleteHandler(auth, targetConnectionManager, reportService); + new TargetRecordingDeleteHandler( + auth, targetConnectionManager, notificationFactory, reportService); } @Test @@ -138,6 +160,14 @@ public Object answer(InvocationOnMock invocation) throws Throwable { InOrder inOrder = Mockito.inOrder(resp); inOrder.verify(resp).setStatusCode(200); inOrder.verify(resp).end(); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("RecordingDeleted"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder) + .message(Map.of("recording", "someRecording", "target", "fooTarget")); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); } @Test diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSaveTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSaveTest.java index e90d94eb54..084a33e581 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSaveTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchSaveTest.java @@ -37,11 +37,14 @@ */ package io.cryostat.net.web.http.api.v1; +import static org.mockito.Mockito.lenient; + import java.io.InputStream; import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Map; import javax.management.remote.JMXServiceURL; @@ -51,8 +54,11 @@ import io.cryostat.core.net.JFRConnection; import io.cryostat.core.sys.Clock; import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.platform.PlatformClient; import io.cryostat.platform.ServiceRef; @@ -81,6 +87,9 @@ class TargetRecordingPatchSaveTest { @Mock TargetConnectionManager targetConnectionManager; @Mock Clock clock; @Mock PlatformClient platformClient; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; @Mock RoutingContext ctx; @Mock HttpServerResponse resp; @@ -94,8 +103,25 @@ class TargetRecordingPatchSaveTest { void setup() { this.patchSave = new TargetRecordingPatchSave( - fs, recordingsPath, targetConnectionManager, clock, platformClient); + fs, + recordingsPath, + targetConnectionManager, + clock, + platformClient, + notificationFactory); Mockito.when(ctx.pathParam("recordingName")).thenReturn(recordingName); + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); } @Test @@ -181,6 +207,19 @@ public Object answer(InvocationOnMock invocation) throws Throwable { String timestamp = now.truncatedTo(ChronoUnit.SECONDS).toString().replaceAll("[-:]+", ""); inOrder.verify(resp).end("some-Alias-2_someRecording_" + timestamp + ".jfr"); Mockito.verify(fs).copy(Mockito.eq(stream), Mockito.eq(destination)); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("RecordingArchived"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder) + .message( + Map.of( + "recording", + "some-Alias-2_someRecording_" + timestamp + ".jfr", + "target", + targetId)); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); } @Test diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java index 8c3ff157c1..5731b32315 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java @@ -37,6 +37,8 @@ */ package io.cryostat.net.web.http.api.v1; +import static org.mockito.Mockito.lenient; + import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -58,9 +60,12 @@ import io.cryostat.commands.internal.RecordingOptionsBuilderFactory; import io.cryostat.core.log.Logger; import io.cryostat.core.net.JFRConnection; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.web.WebServer; +import io.cryostat.net.web.http.HttpMimeType; import com.google.gson.Gson; import io.vertx.core.MultiMap; @@ -93,6 +98,9 @@ class TargetRecordingsPostHandlerTest { @Mock EventOptionsBuilder.Factory eventOptionsBuilderFactory; @Mock WebServer webServer; @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; Gson gson = MainModule.provideGson(logger); @Mock JFRConnection connection; @@ -110,7 +118,20 @@ void setup() { recordingOptionsBuilderFactory, eventOptionsBuilderFactory, () -> webServer, - gson); + gson, + notificationFactory); + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); } @Test @@ -223,6 +244,14 @@ void shouldStartRecording() throws Exception { MatcherAssert.assertThat(eventCaptor.getValue(), Matchers.equalTo("foo.Bar")); MatcherAssert.assertThat(optionCaptor.getValue(), Matchers.equalTo("enabled")); MatcherAssert.assertThat(valueCaptor.getValue(), Matchers.equalTo("true")); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("RecordingCreated"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder) + .message(Map.of("recording", "someRecording", "target", "fooHost:9091")); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); } @Test diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java index 2aac4281c2..faf497ee1d 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java @@ -37,10 +37,16 @@ */ package io.cryostat.net.web.http.api.v1; +import static org.mockito.Mockito.lenient; + import java.io.IOException; +import java.util.Map; import io.cryostat.core.templates.LocalStorageTemplateService; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; +import io.cryostat.net.web.http.HttpMimeType; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; @@ -61,10 +67,25 @@ class TemplateDeleteHandlerTest { TemplateDeleteHandler handler; @Mock AuthManager auth; @Mock LocalStorageTemplateService templateService; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; @BeforeEach void setup() { - this.handler = new TemplateDeleteHandler(auth, templateService); + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = new TemplateDeleteHandler(auth, templateService, notificationFactory); } @Test @@ -99,5 +120,12 @@ void shouldCallThroughToService() throws Exception { Mockito.verify(templateService).deleteTemplate("FooTemplate"); Mockito.verify(ctx).response(); Mockito.verify(resp).end(); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("TemplateDeleted"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder).message(Map.of("template", "FooTemplate")); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); } } diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java index b39df0cb26..211f452068 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java @@ -37,16 +37,22 @@ */ package io.cryostat.net.web.http.api.v1; +import static org.mockito.Mockito.lenient; + import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; +import java.util.Map; import java.util.Set; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; import io.cryostat.core.templates.LocalStorageTemplateService; import io.cryostat.core.templates.MutableTemplateService.InvalidXmlException; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; +import io.cryostat.net.web.http.HttpMimeType; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerResponse; @@ -71,10 +77,26 @@ class TemplatesPostHandlerTest { @Mock LocalStorageTemplateService templateService; @Mock FileSystem fs; @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; @BeforeEach void setup() { - this.handler = new TemplatesPostHandler(auth, templateService, fs, logger); + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new TemplatesPostHandler(auth, templateService, fs, notificationFactory, logger); } @Test @@ -163,4 +185,41 @@ void shouldProcessGoodRequest() throws Exception { Mockito.verify(ctx).response(); Mockito.verify(resp).end(); } + + @Test + void shouldSendNotifcationOnTemplateDeletion() throws Exception { + RoutingContext ctx = Mockito.mock(RoutingContext.class); + + HttpServerResponse resp = Mockito.mock(HttpServerResponse.class); + Mockito.when(ctx.response()).thenReturn(resp); + + FileUpload upload1 = Mockito.mock(FileUpload.class); + Mockito.when(upload1.name()).thenReturn("template"); + Mockito.when(upload1.uploadedFileName()).thenReturn("/file-uploads/abcd-1234"); + + FileUpload upload2 = Mockito.mock(FileUpload.class); + Mockito.when(upload2.name()).thenReturn("unused"); + Mockito.when(upload2.uploadedFileName()).thenReturn("/file-uploads/wxyz-9999"); + + Mockito.when(ctx.fileUploads()).thenReturn(Set.of(upload1, upload2)); + + Path uploadPath1 = Mockito.mock(Path.class); + Path uploadPath2 = Mockito.mock(Path.class); + Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath1); + Mockito.when(fs.pathOf("/file-uploads/wxyz-9999")).thenReturn(uploadPath2); + + InputStream stream1 = Mockito.mock(InputStream.class); + InputStream stream2 = Mockito.mock(InputStream.class); + Mockito.when(fs.newInputStream(uploadPath1)).thenReturn(stream1); + Mockito.when(fs.newInputStream(uploadPath2)).thenReturn(stream2); + + handler.handleAuthenticated(ctx); + + Mockito.verify(notificationFactory).createBuilder(); + Mockito.verify(notificationBuilder).metaCategory("TemplateUploaded"); + Mockito.verify(notificationBuilder).metaType(HttpMimeType.JSON); + Mockito.verify(notificationBuilder).message(Map.of("template", "/file-uploads/abcd-1234")); + Mockito.verify(notificationBuilder).build(); + Mockito.verify(notification).send(); + } }