From 8172ff61d8d8f69954bdc2d4784d8f978d6ddfbd Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 25 Jan 2021 11:07:04 -0500 Subject: [PATCH] Add PeriodicArchiverTest --- .../containerjfr/rules/PeriodicArchiver.java | 7 +- .../rules/PeriodicArchiverTest.java | 196 ++++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiverTest.java diff --git a/src/main/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiver.java b/src/main/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiver.java index 7f3bd9dce5..26de78af5d 100644 --- a/src/main/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiver.java +++ b/src/main/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiver.java @@ -92,6 +92,8 @@ class PeriodicArchiver implements Runnable { this.rule = rule; this.logger = logger; + // FIXME this needs to be populated at startup by scanning the existing archived recordings, + // in case we have been restarted and already previously processed archival for this rule this.previousRecordings = new ArrayDeque<>(this.rule.getPreservedArchives()); } @@ -100,8 +102,10 @@ public void run() { logger.trace(String.format("PeriodicArchiver for %s running", rule.getRecordingName())); try { + // FIXME why is this needed if the data structure is a deque, which already has a capped + // size? while (this.previousRecordings.size() > this.rule.getPreservedArchives() - 1) { - pruneArchive(this.previousRecordings.remove()); + pruneArchive(this.previousRecordings.remove()).get(); } performArchival(); @@ -176,6 +180,7 @@ Future pruneArchive(String recordingName) { ":recordingName", URLEncodedUtils.formatSegments(recordingName))) .normalize(); + // TODO refactor and extract this header creation MultiMap headers = MultiMap.caseInsensitiveMultiMap(); if (credentials != null) { headers.add( diff --git a/src/test/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiverTest.java b/src/test/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiverTest.java new file mode 100644 index 0000000000..d0b079c307 --- /dev/null +++ b/src/test/java/com/redhat/rhjmc/containerjfr/rules/PeriodicArchiverTest.java @@ -0,0 +1,196 @@ +/*- + * #%L + * Container JFR + * %% + * Copyright (C) 2020 Red Hat, Inc. + * %% + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * #L% + */ +package com.redhat.rhjmc.containerjfr.rules; + +import java.util.Base64; + +import javax.management.remote.JMXServiceURL; + +import com.redhat.rhjmc.containerjfr.core.log.Logger; +import com.redhat.rhjmc.containerjfr.core.net.Credentials; +import com.redhat.rhjmc.containerjfr.net.web.http.AbstractAuthenticatedRequestHandler; +import com.redhat.rhjmc.containerjfr.platform.ServiceRef; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; + +@ExtendWith(MockitoExtension.class) +class PeriodicArchiverTest { + + PeriodicArchiver archiver; + String jmxUrl = "service:jmx:rmi://localhost:9091/jndi/rmi://fooHost:9091/jmxrmi"; + ServiceRef serviceRef; + Credentials credentials = new Credentials("foouser", "barpassword"); + Rule rule = + new Rule.Builder() + .name("Test Rule") + .description("Automated unit test rule") + .targetAlias("com.example.App") + .eventSpecifier("template=Continuous") + .maxAgeSeconds(30) + .maxSizeBytes(1234) + .preservedArchives(2) + .archivalPeriodSeconds(67) + .build(); + @Mock WebClient webClient; + String archiveRequestPath = "/api/v1/targets/:targetId/recordings/:recordingName"; + String deleteRequestPath = "/api/v1/recordings/:recordingName"; + @Mock Logger logger; + + @BeforeEach + void setup() throws Exception { + this.serviceRef = new ServiceRef(new JMXServiceURL(jmxUrl), "com.example.App"); + this.archiver = + new PeriodicArchiver( + serviceRef, + credentials, + rule, + webClient, + archiveRequestPath, + deleteRequestPath, + logger); + } + + @Test + void testPerformArchival() throws Exception { + HttpRequest request = Mockito.mock(HttpRequest.class); + HttpResponse response = Mockito.mock(HttpResponse.class); + Mockito.when(response.statusCode()).thenReturn(200); + Mockito.when(response.bodyAsString()).thenReturn(rule.getRecordingName() + "_1"); + + Mockito.when(webClient.patch(Mockito.any())).thenReturn(request); + Mockito.when(request.putHeaders(Mockito.any())).thenReturn(request); + Mockito.doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + AsyncResult res = Mockito.mock(AsyncResult.class); + Mockito.when(res.failed()).thenReturn(false); + Mockito.when(res.result()).thenReturn(response); + ((Handler) invocation.getArgument(1)).handle(res); + return null; + } + }) + .when(request) + .sendBuffer(Mockito.any(), Mockito.any()); + + archiver.performArchival(); + + ArgumentCaptor patchActionCaptor = ArgumentCaptor.forClass(Buffer.class); + Mockito.verify(request).sendBuffer(patchActionCaptor.capture(), Mockito.any()); + Buffer patchAction = patchActionCaptor.getValue(); + MatcherAssert.assertThat(patchAction.toString(), Matchers.equalTo("save")); + + ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + Mockito.verify(request).putHeaders(headersCaptor.capture()); + MultiMap headers = headersCaptor.getValue(); + MatcherAssert.assertThat( + headers.get(AbstractAuthenticatedRequestHandler.JMX_AUTHORIZATION_HEADER), + Matchers.equalTo( + "Basic " + + Base64.getEncoder() + .encodeToString("foouser:barpassword".getBytes()))); + + Mockito.verify(webClient) + .patch( + "/api/v1/targets/service:jmx:rmi:%2F%2Flocalhost:9091%2Fjndi%2Frmi:%2F%2FfooHost:9091%2Fjmxrmi/recordings/auto_Test_Rule"); + } + + @Test + void testPruneArchive() throws Exception { + // get the archiver into a state where it is tracking a previously-archived recording + testPerformArchival(); + + HttpRequest request = Mockito.mock(HttpRequest.class); + HttpResponse response = Mockito.mock(HttpResponse.class); + Mockito.when(response.statusCode()).thenReturn(200); + + Mockito.when(webClient.delete(Mockito.any())).thenReturn(request); + Mockito.when(request.putHeaders(Mockito.any())).thenReturn(request); + Mockito.doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + AsyncResult res = Mockito.mock(AsyncResult.class); + Mockito.when(res.failed()).thenReturn(false); + Mockito.when(res.result()).thenReturn(response); + ((Handler) invocation.getArgument(0)).handle(res); + return null; + } + }) + .when(request) + .send(Mockito.any()); + + boolean result = archiver.pruneArchive(rule.getRecordingName() + "_1").get(); + Assertions.assertTrue(result); + + ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + Mockito.verify(request).putHeaders(headersCaptor.capture()); + MultiMap headers = headersCaptor.getValue(); + MatcherAssert.assertThat( + headers.get(AbstractAuthenticatedRequestHandler.JMX_AUTHORIZATION_HEADER), + Matchers.equalTo( + "Basic " + + Base64.getEncoder() + .encodeToString("foouser:barpassword".getBytes()))); + + Mockito.verify(webClient).delete("/api/v1/recordings/auto_Test_Rule_1"); + } +}