Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e114897

Browse files
committedDec 26, 2023
test(clouddriver): Add tests to verify the failure cases of MontioredDeployTask and 'getRetrofitLogMessage()'
This commit sets the stage for forthcoming changes currently in progress under PR : spinnaker#4614 . The primary goal is to compare the behaviour before and after enhancements by introducing test cases for the ‘MonitoredDeployTask’ and the ‘getRetrofitLogMessage()’ method. For ‘MonitoredDeployTask’: - Test Case to simulate networkError and observe behaviour - Test Case to simulate httpError and observe behaviour - Test Case to simulate unexpectedError and observe behaviour - Test Case to simulate conversionError and observe behaviour For ‘getRetrofitLogMessage()’: - Test Cases to verify behaviour during HTTP error details parsing when exceptions occur Additionally, a Mockito dependency ('org.mockito:mockito-inline') has been added to support spying/mocking on the final class 'retrofit.client.Response'. This resolves the issue encountered during testing where Mockito couldn't mock/spy the final class, preventing the following error: org.mockito.exceptions.base.MockitoException: Cannot mock/spy class retrofit.client.Response Mockito cannot mock/spy because : - final class at com.netflix.spinnaker.orca.clouddriver.tasks.monitoreddeploy.MonitoredDeployBaseTaskTest.shouldReturnOnlyStatusWhenExceptionThrownWhileParsingHttpErrorBody(MonitoredDeployBaseTaskTest.java:217) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688) at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:131)
1 parent d382b38 commit e114897

File tree

3 files changed

+264
-1
lines changed

3 files changed

+264
-1
lines changed
 

‎orca-clouddriver/orca-clouddriver.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ dependencies {
5959
testImplementation("io.strikt:strikt-core")
6060
testImplementation("io.mockk:mockk")
6161
testImplementation("ru.lanwen.wiremock:wiremock-junit5:1.3.1")
62+
testImplementation("org.mockito:mockito-inline")
6263

6364
testCompileOnly("org.projectlombok:lombok")
6465
testAnnotationProcessor("org.projectlombok:lombok")

‎orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/monitoreddeploy/EvaluateDeploymentHealthTaskSpec.groovy

+203-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package com.netflix.spinnaker.orca.clouddriver.tasks.monitoreddeploy
1818

19+
import com.fasterxml.jackson.databind.ObjectMapper
1920
import com.netflix.spectator.api.NoopRegistry
2021
import com.netflix.spinnaker.config.DeploymentMonitorDefinition
2122
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus
2223
import com.netflix.spinnaker.orca.api.pipeline.TaskResult
24+
import com.netflix.spinnaker.orca.clouddriver.MortServiceSpec
2325
import com.netflix.spinnaker.orca.deploymentmonitor.DeploymentMonitorService
2426
import com.netflix.spinnaker.orca.deploymentmonitor.models.DeploymentMonitorStageConfig
2527
import com.netflix.spinnaker.orca.deploymentmonitor.models.DeploymentStep
@@ -28,9 +30,13 @@ import com.netflix.spinnaker.orca.deploymentmonitor.models.MonitoredDeployIntern
2830
import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl
2931
import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl
3032
import retrofit.RetrofitError
33+
import retrofit.client.Response
34+
import retrofit.converter.ConversionException
35+
import retrofit.converter.JacksonConverter
3136
import spock.lang.Specification
3237
import com.netflix.spinnaker.orca.deploymentmonitor.DeploymentMonitorServiceProvider
3338
import spock.lang.Unroll
39+
import org.springframework.http.HttpStatus
3440

3541
import java.time.Instant
3642
import java.util.concurrent.TimeUnit
@@ -41,7 +47,7 @@ class EvaluateDeploymentHealthTaskSpec extends Specification {
4147
PipelineExecutionImpl pipe = pipeline {
4248
}
4349

44-
def "should retry retrofit errors"() {
50+
def "should handle retrofit network error and return the task status depending on the scenarios"() {
4551
given:
4652
def monitorServiceStub = Stub(DeploymentMonitorService) {
4753
evaluateHealth(_) >> {
@@ -198,6 +204,202 @@ class EvaluateDeploymentHealthTaskSpec extends Specification {
198204
false | null || ExecutionStatus.FAILED_CONTINUE
199205
}
200206

207+
def "should handle retrofit http error and return the task status depending on the scenarios"() {
208+
209+
def converter = new JacksonConverter(new ObjectMapper())
210+
211+
Response response =
212+
new Response(
213+
"/deployment/evaluateHealth",
214+
HttpStatus.BAD_REQUEST.value(),
215+
"bad-request",
216+
Collections.emptyList(),
217+
new MortServiceSpec.MockTypedInput(converter, [
218+
accountName: "account",
219+
description: "simple description",
220+
name: "sg1",
221+
region: "region",
222+
type: "openstack"
223+
]))
224+
225+
given:
226+
def monitorServiceStub = Stub(DeploymentMonitorService) {
227+
evaluateHealth(_) >> {
228+
throw RetrofitError.httpError("https://foo.com/deployment/evaluateHealth", response, converter, null)
229+
}
230+
}
231+
232+
def serviceProviderStub = getServiceProviderStub(monitorServiceStub)
233+
234+
def task = new EvaluateDeploymentHealthTask(serviceProviderStub, new NoopRegistry())
235+
236+
MonitoredDeployInternalStageData stageData = new MonitoredDeployInternalStageData()
237+
stageData.deploymentMonitor = new DeploymentMonitorStageConfig()
238+
stageData.deploymentMonitor.id = "LogMonitorId"
239+
240+
def stage = new StageExecutionImpl(pipe, "evaluateDeploymentHealth", stageData.toContextMap() + [application: pipe.application])
241+
stage.startTime = Instant.now().toEpochMilli()
242+
243+
when: 'we can still retry'
244+
TaskResult result = task.execute(stage)
245+
246+
then: 'should retry'
247+
result.status == ExecutionStatus.RUNNING
248+
result.context.deployMonitorHttpRetryCount == 1
249+
250+
when: 'we ran out of retries'
251+
stage.context.deployMonitorHttpRetryCount = MonitoredDeployBaseTask.MAX_RETRY_COUNT
252+
result = task.execute(stage)
253+
254+
then: 'should terminate'
255+
result.status == ExecutionStatus.TERMINAL
256+
257+
when: 'we ran out of retries and failOnError = false'
258+
serviceProviderStub = getServiceProviderStub(monitorServiceStub, {DeploymentMonitorDefinition dm -> dm.failOnError = false})
259+
task = new EvaluateDeploymentHealthTask(serviceProviderStub, new NoopRegistry())
260+
result = task.execute(stage)
261+
262+
then: 'should return fail_continue'
263+
result.status == ExecutionStatus.FAILED_CONTINUE
264+
265+
when: 'we ran out of retries and failOnError = false but there is a stage override for failOnError=true'
266+
stageData.deploymentMonitor.failOnErrorOverride = true
267+
stage = new StageExecutionImpl(pipe, "evaluateDeploymentHealth", stageData.toContextMap() + [
268+
application: pipe.application,
269+
deployMonitorHttpRetryCount: MonitoredDeployBaseTask.MAX_RETRY_COUNT
270+
])
271+
stage.startTime = Instant.now().toEpochMilli()
272+
result = task.execute(stage)
273+
274+
then: 'should terminate'
275+
result.status == ExecutionStatus.TERMINAL
276+
}
277+
278+
def "should handle retrofit conversion error and return the task status depending on the scenarios"() {
279+
def converter = new JacksonConverter(new ObjectMapper())
280+
281+
Response response =
282+
new Response(
283+
"/deployment/evaluateHealth",
284+
HttpStatus.BAD_REQUEST.value(),
285+
"bad-request",
286+
Collections.emptyList(),
287+
new MortServiceSpec.MockTypedInput(converter, [
288+
accountName: "account",
289+
description: "simple description",
290+
name: "sg1",
291+
region: "region",
292+
type: "openstack"
293+
]))
294+
295+
given:
296+
def monitorServiceStub = Stub(DeploymentMonitorService) {
297+
evaluateHealth(_) >> {
298+
throw RetrofitError.conversionError("https://foo.com/deployment/evaluateHealth", response, converter, null, new ConversionException("Failed to parse/convert the error response body"))
299+
}
300+
}
301+
302+
def serviceProviderStub = getServiceProviderStub(monitorServiceStub)
303+
304+
def task = new EvaluateDeploymentHealthTask(serviceProviderStub, new NoopRegistry())
305+
306+
MonitoredDeployInternalStageData stageData = new MonitoredDeployInternalStageData()
307+
stageData.deploymentMonitor = new DeploymentMonitorStageConfig()
308+
stageData.deploymentMonitor.id = "LogMonitorId"
309+
310+
def stage = new StageExecutionImpl(pipe, "evaluateDeploymentHealth", stageData.toContextMap() + [application: pipe.application])
311+
stage.startTime = Instant.now().toEpochMilli()
312+
313+
when: 'we can still retry'
314+
TaskResult result = task.execute(stage)
315+
316+
then: 'should retry'
317+
result.status == ExecutionStatus.RUNNING
318+
result.context.deployMonitorHttpRetryCount == 1
319+
320+
when: 'we ran out of retries'
321+
stage.context.deployMonitorHttpRetryCount = MonitoredDeployBaseTask.MAX_RETRY_COUNT
322+
result = task.execute(stage)
323+
324+
then: 'should terminate'
325+
result.status == ExecutionStatus.TERMINAL
326+
327+
when: 'we ran out of retries and failOnError = false'
328+
serviceProviderStub = getServiceProviderStub(monitorServiceStub, {DeploymentMonitorDefinition dm -> dm.failOnError = false})
329+
task = new EvaluateDeploymentHealthTask(serviceProviderStub, new NoopRegistry())
330+
result = task.execute(stage)
331+
332+
then: 'should return fail_continue'
333+
result.status == ExecutionStatus.FAILED_CONTINUE
334+
335+
when: 'we ran out of retries and failOnError = false but there is a stage override for failOnError=true'
336+
stageData.deploymentMonitor.failOnErrorOverride = true
337+
stage = new StageExecutionImpl(pipe, "evaluateDeploymentHealth", stageData.toContextMap() + [
338+
application: pipe.application,
339+
deployMonitorHttpRetryCount: MonitoredDeployBaseTask.MAX_RETRY_COUNT
340+
])
341+
stage.startTime = Instant.now().toEpochMilli()
342+
result = task.execute(stage)
343+
344+
then: 'should terminate'
345+
result.status == ExecutionStatus.TERMINAL
346+
}
347+
348+
def "should handle retrofit unexpected error and return the task status depending on the scenarios"() {
349+
350+
given:
351+
def monitorServiceStub = Stub(DeploymentMonitorService) {
352+
evaluateHealth(_) >> {
353+
throw RetrofitError.unexpectedError("url", new IOException())
354+
}
355+
}
356+
357+
def serviceProviderStub = getServiceProviderStub(monitorServiceStub)
358+
359+
def task = new EvaluateDeploymentHealthTask(serviceProviderStub, new NoopRegistry())
360+
361+
MonitoredDeployInternalStageData stageData = new MonitoredDeployInternalStageData()
362+
stageData.deploymentMonitor = new DeploymentMonitorStageConfig()
363+
stageData.deploymentMonitor.id = "LogMonitorId"
364+
365+
def stage = new StageExecutionImpl(pipe, "evaluateDeploymentHealth", stageData.toContextMap() + [application: pipe.application])
366+
stage.startTime = Instant.now().toEpochMilli()
367+
368+
when: 'we can still retry'
369+
TaskResult result = task.execute(stage)
370+
371+
then: 'should retry'
372+
result.status == ExecutionStatus.RUNNING
373+
result.context.deployMonitorHttpRetryCount == 1
374+
375+
when: 'we ran out of retries'
376+
stage.context.deployMonitorHttpRetryCount = MonitoredDeployBaseTask.MAX_RETRY_COUNT
377+
result = task.execute(stage)
378+
379+
then: 'should terminate'
380+
result.status == ExecutionStatus.TERMINAL
381+
382+
when: 'we ran out of retries and failOnError = false'
383+
serviceProviderStub = getServiceProviderStub(monitorServiceStub, {DeploymentMonitorDefinition dm -> dm.failOnError = false})
384+
task = new EvaluateDeploymentHealthTask(serviceProviderStub, new NoopRegistry())
385+
result = task.execute(stage)
386+
387+
then: 'should return fail_continue'
388+
result.status == ExecutionStatus.FAILED_CONTINUE
389+
390+
when: 'we ran out of retries and failOnError = false but there is a stage override for failOnError=true'
391+
stageData.deploymentMonitor.failOnErrorOverride = true
392+
stage = new StageExecutionImpl(pipe, "evaluateDeploymentHealth", stageData.toContextMap() + [
393+
application: pipe.application,
394+
deployMonitorHttpRetryCount: MonitoredDeployBaseTask.MAX_RETRY_COUNT
395+
])
396+
stage.startTime = Instant.now().toEpochMilli()
397+
result = task.execute(stage)
398+
399+
then: 'should terminate'
400+
result.status == ExecutionStatus.TERMINAL
401+
}
402+
201403
private getServiceProviderStub(monitorServiceStub) {
202404
return getServiceProviderStub(monitorServiceStub, {})
203405
}

‎orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/monitoreddeploy/MonitoredDeployBaseTaskTest.java

+60
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
package com.netflix.spinnaker.orca.clouddriver.tasks.monitoreddeploy;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
2022

2123
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import com.google.common.io.CharStreams;
2225
import com.netflix.spectator.api.NoopRegistry;
2326
import com.netflix.spinnaker.config.DeploymentMonitorDefinition;
2427
import com.netflix.spinnaker.orca.deploymentmonitor.DeploymentMonitorServiceProvider;
2528
import java.io.ByteArrayInputStream;
2629
import java.io.ByteArrayOutputStream;
2730
import java.io.IOException;
2831
import java.io.InputStream;
32+
import java.io.InputStreamReader;
33+
import java.nio.charset.StandardCharsets;
2934
import java.util.ArrayList;
3035
import java.util.HashMap;
3136
import org.junit.jupiter.api.BeforeEach;
@@ -168,6 +173,61 @@ void shouldReturnDefaultLogMsgWhenUnexpectedErrorHasOccurred() {
168173
assertThat(logMessageOnUnexpectedError).isEqualTo("<NO RESPONSE>");
169174
}
170175

176+
@Test
177+
void returnsEmptyHttpErrorDetailsWhenExceptionReadingHttpStatus() {
178+
179+
Response response = mock(Response.class);
180+
181+
when(response.getStatus()).thenThrow(IllegalArgumentException.class);
182+
183+
String logMessageOnHttpError = monitoredDeployBaseTask.getRetrofitLogMessage(response);
184+
185+
assertThat(logMessageOnHttpError).isEqualTo("status: \nheaders: \nresponse body: ");
186+
}
187+
188+
@Test
189+
void returnsOnlyStatusWhenExceptionParsingHttpErrorBody() {
190+
191+
Response response = mock(Response.class);
192+
193+
when(response.getStatus()).thenReturn(HttpStatus.BAD_REQUEST.value()); // arbitrary
194+
when(response.getReason()).thenReturn("arbitrary reason");
195+
when(response.getBody()).thenThrow(IllegalArgumentException.class);
196+
197+
String logMessageOnHttpError = monitoredDeployBaseTask.getRetrofitLogMessage(response);
198+
199+
String status = String.format("%d (%s)", response.getStatus(), response.getReason());
200+
201+
assertThat(logMessageOnHttpError)
202+
.isEqualTo(String.format("status: %s\nheaders: \nresponse body: ", status));
203+
}
204+
205+
@Test
206+
void returnsOnlyStatusAndBodyWhenExceptionReadingHttpHeaders() throws IOException {
207+
208+
var converter = new JacksonConverter(objectMapper);
209+
var responseBody = new HashMap<String, String>();
210+
211+
responseBody.put("error", "400 - Bad request, application name cannot be empty");
212+
213+
Response response = mock(Response.class);
214+
215+
when(response.getStatus()).thenReturn(HttpStatus.BAD_REQUEST.value()); // arbitrary
216+
when(response.getReason()).thenReturn("arbitrary reason");
217+
when(response.getBody()).thenReturn(new MockTypedInput(converter, responseBody));
218+
when(response.getHeaders()).thenThrow(IllegalArgumentException.class);
219+
220+
String logMessageOnHttpError = monitoredDeployBaseTask.getRetrofitLogMessage(response);
221+
222+
String status = String.format("%d (%s)", response.getStatus(), response.getReason());
223+
String body =
224+
CharStreams.toString(
225+
new InputStreamReader(response.getBody().in(), StandardCharsets.UTF_8));
226+
227+
assertThat(logMessageOnHttpError)
228+
.isEqualTo(String.format("status: %s\nheaders: \nresponse body: %s", status, body));
229+
}
230+
171231
static class MockTypedInput implements TypedInput {
172232
private final Converter converter;
173233
private final Object body;

0 commit comments

Comments
 (0)
Please sign in to comment.