Skip to content

Commit 53bfc87

Browse files
committed
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 : #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:2.13.0') 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 adc81ac commit 53bfc87

File tree

3 files changed

+300
-1
lines changed

3 files changed

+300
-1
lines changed

orca-clouddriver/orca-clouddriver.gradle

Lines changed: 1 addition & 0 deletions
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:2.13.0'
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

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

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

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,22 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020

2121
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.google.common.io.CharStreams;
2223
import com.netflix.spectator.api.NoopRegistry;
2324
import com.netflix.spinnaker.config.DeploymentMonitorDefinition;
2425
import com.netflix.spinnaker.orca.deploymentmonitor.DeploymentMonitorServiceProvider;
2526
import java.io.ByteArrayInputStream;
2627
import java.io.ByteArrayOutputStream;
2728
import java.io.IOException;
2829
import java.io.InputStream;
30+
import java.io.InputStreamReader;
31+
import java.nio.charset.StandardCharsets;
2932
import java.util.ArrayList;
33+
import java.util.Collections;
3034
import java.util.HashMap;
3135
import org.junit.jupiter.api.BeforeEach;
3236
import org.junit.jupiter.api.Test;
37+
import org.mockito.Mockito;
3338
import org.springframework.http.HttpHeaders;
3439
import org.springframework.http.HttpStatus;
3540
import org.springframework.http.MediaType;
@@ -168,6 +173,99 @@ void shouldReturnDefaultLogMsgWhenUnexpectedErrorHasOccurred() {
168173
assertThat(logMessageOnUnexpectedError).isEqualTo("<NO RESPONSE>");
169174
}
170175

176+
@Test
177+
void shouldReturnEmptyHttpErrorDetailsWhenExceptionThrownWhileReadingHttpStatus() {
178+
179+
var converter = new JacksonConverter(objectMapper);
180+
var responseBody = new HashMap<String, String>();
181+
182+
responseBody.put("error", "400 - Bad request, application name cannot be empty");
183+
184+
Response response =
185+
Mockito.spy(
186+
new Response(
187+
"/deployment/evaluateHealth",
188+
HttpStatus.BAD_REQUEST.value(),
189+
HttpStatus.BAD_REQUEST.name(),
190+
Collections.emptyList(),
191+
new MockTypedInput(converter, responseBody)));
192+
193+
Mockito.when(response.getStatus()).thenThrow(IllegalArgumentException.class);
194+
195+
String logMessageOnHttpError = monitoredDeployBaseTask.getRetrofitLogMessage(response);
196+
197+
String status = "";
198+
String body = "";
199+
String headers = "";
200+
201+
assertThat(logMessageOnHttpError)
202+
.isEqualTo(
203+
String.format("status: %s\nheaders: %s\nresponse body: %s", status, headers, body));
204+
}
205+
206+
@Test
207+
void shouldReturnOnlyStatusWhenExceptionThrownWhileParsingHttpErrorBody() {
208+
209+
var converter = new JacksonConverter(objectMapper);
210+
var responseBody = new HashMap<String, String>();
211+
212+
responseBody.put("error", "400 - Bad request, application name cannot be empty");
213+
214+
Response response =
215+
Mockito.spy(
216+
new Response(
217+
"/deployment/evaluateHealth",
218+
HttpStatus.BAD_REQUEST.value(),
219+
HttpStatus.BAD_REQUEST.name(),
220+
Collections.emptyList(),
221+
new MockTypedInput(converter, responseBody)));
222+
223+
Mockito.when(response.getBody()).thenThrow(IllegalArgumentException.class);
224+
225+
String logMessageOnHttpError = monitoredDeployBaseTask.getRetrofitLogMessage(response);
226+
227+
String status = String.format("%d (%s)", response.getStatus(), response.getReason());
228+
String body = "";
229+
String headers = "";
230+
231+
assertThat(logMessageOnHttpError)
232+
.isEqualTo(
233+
String.format("status: %s\nheaders: %s\nresponse body: %s", status, headers, body));
234+
}
235+
236+
@Test
237+
void shouldReturnOnlyStatusAndBodyWhenExceptionThrownWhileReadingHttpHeaders()
238+
throws IOException {
239+
240+
var converter = new JacksonConverter(objectMapper);
241+
var responseBody = new HashMap<String, String>();
242+
243+
responseBody.put("error", "400 - Bad request, application name cannot be empty");
244+
245+
Response response =
246+
Mockito.spy(
247+
new Response(
248+
"/deployment/evaluateHealth",
249+
HttpStatus.BAD_REQUEST.value(),
250+
HttpStatus.BAD_REQUEST.name(),
251+
Collections.emptyList(),
252+
new MockTypedInput(converter, responseBody)));
253+
254+
Mockito.when(response.getHeaders()).thenThrow(IllegalArgumentException.class);
255+
256+
String logMessageOnHttpError = monitoredDeployBaseTask.getRetrofitLogMessage(response);
257+
258+
String status = String.format("%d (%s)", response.getStatus(), response.getReason());
259+
String body =
260+
CharStreams.toString(
261+
new InputStreamReader(response.getBody().in(), StandardCharsets.UTF_8));
262+
String headers = "";
263+
264+
assertThat(logMessageOnHttpError)
265+
.isEqualTo(
266+
String.format("status: %s\nheaders: %s\nresponse body: %s", status, headers, body));
267+
}
268+
171269
static class MockTypedInput implements TypedInput {
172270
private final Converter converter;
173271
private final Object body;

0 commit comments

Comments
 (0)